Last week I showed you how you can use Wiremock in a docker container to mock API calls that your application uses. This week is on a similar theme, but this time using WireMock.net to help with your integration tests.
Unlike WireMock which is Java-based, as you can guess from the name WireMock.Net is written in .NET and based on mock4net. Everything you can do in Wiremock you can also do in WireMock.net. This also includes hosting it in a docker container like we did in the last post (the ASCII art is missing though).
In this tutorial, we are going to be looking at using WireMock.net to mock API calls in our integration tests.
Getting started
First of all, we need a .Net project to test. For this project I am just going to create a new .Net Core Web API from the templates:
dotnet new webapi
This will generate a simple API that gives us a random weather forecast.
Running dotnet run
and then calling the GET endpoint https://localhost:5001/WeatherForecast
returns the following:
[
{
"date": "2021-05-22T10:22:17.824198+01:00",
"temperatureC": 36,
"temperatureF": 96,
"summary": "Chilly"
},
{
"date": "2021-05-23T10:22:17.824551+01:00",
"temperatureC": 25,
"temperatureF": 76,
"summary": "Warm"
},
{
"date": "2021-05-24T10:22:17.824557+01:00",
"temperatureC": 20,
"temperatureF": 67,
"summary": "Hot"
},
{
"date": "2021-05-25T10:22:17.824557+01:00",
"temperatureC": 14,
"temperatureF": 57,
"summary": "Bracing"
},
{
"date": "2021-05-26T10:22:17.824558+01:00",
"temperatureC": -15,
"temperatureF": 6,
"summary": "Sweltering"
}
]
You can tell it is random, as I wouldn’t consider -15°C, “Sweltering”.
The actual code that generates this looks like this:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering",
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable<WeatherForecast> Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
There is one subtle change that has to be made to be able to run integration tests against this. That is to replace the IHostBuilder
in Program.cs
with an IWebHostBuilder
.
Your Program.cs
should look like this:
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace WireMock.Net.Api
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args).UseStartup<Startup>();
}
}
Adding an API to Test
The whole point of an integration test is to test the integration between components. So we need to add an API to our project so that we can mock it using wiremock.net.
As this API is already returning random weather, we are going to add in real weather API to get back realistic data. For this, we are going to use 7timer’s weather API. This API is free and doesn’t require any authentication.
The API we are going to call is this one:
https://www.7timer.info/bin/civillight.php?lat=51.5074&lon=0.1278&ac=0&unit=metric&output=json&tzshift=0
This returns back weather for the next 7 days for London (51.5074° N, 0.1278° W). The format of the data we get back is slightly different so we will need to format it before returning the data. This is what the first few entries in the array look like:
{
"product": "civillight",
"init": "2021052100",
"dataseries": [
{
"date": 20210521,
"weather": "lightrain",
"temp2m": { "max": 11, "min": 9 },
"wind10m_max": 4
},
{
"date": 20210522,
"weather": "lightrain",
"temp2m": { "max": 10, "min": 7 },
"wind10m_max": 3
},
{
"date": 20210523,
"weather": "lightrain",
"temp2m": { "max": 12, "min": 4 },
"wind10m_max": 4
}
]
}
For our API we need the following information:
-
dataseries[].date
- The date which we can just convert to a DateTime. -
dataseries[].temp2m
- This is the min max temperature in Celsius so we can just use this as is. We can update our API to return both. -
dataseries[].weather
- The weather summary, again we can use as is.
I am going to use Refit for calling the API. If you haven’t used Refit it is a great tool for standardising your API calls and saves on a lot of boilerplate code for calling your API. So add the following packages to your API project:
<PackageReference Include="Refit" Version="6.0.38"/>
<PackageReference Include="Refit.HttpClientFactory" Version="6.0.38"/>
Next we need to create an interface for our API:
using System.Threading;
using System.Threading.Tasks;
using Refit;
using WireMock.Net.Api.Client.Model;
namespace WireMock.Net.Api.Client
{
public interface IWeatherClient
{
[Get("/bin/civillight.php?lat={latitude}&lon={longitude}&ac=0&unit=metric&output=json&tzshift=0")]
Task<ApiResponse<WeatherResponse>> GetWeatherAsync(decimal latitude, decimal longitude, CancellationToken ct);
}
}
My WeatherResponse class looks like this:
using System.Collections.Generic;
using Newtonsoft.Json;
namespace WireMock.Net.Api.Client.Model
{
public class WeatherResponse
{
[JsonProperty("product")]
public string Product { get; set; }
[JsonProperty("init")]
public string Init { get; set; }
[JsonProperty("dataseries")]
public IEnumerable<WeatherData> Dataseries { get; set; }
}
public class WeatherData
{
[JsonProperty("date")]
public int Date { get; set; }
[JsonProperty("weather")]
public string Weather { get; set; }
[JsonProperty("temp2m")]
public Temperature Temp2m { get; set; }
}
public class Temperature
{
[JsonProperty("max")]
public int Max { get; set; }
[JsonProperty("min")]
public int Min { get; set; }
}
}
We are also going to set up a configuration section in our appsettings.json
so that the base address can be mocked later.
{
"WeatherClient": {
"BaseAddress": "https://www.7timer.info",
"TimeoutSeconds": 30
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
So now we need to set up the refit HttpClient. We can do this inside ConfigureServices. To make things nicer I often put the code inside an extension method:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddWeatherClient(this IServiceCollection services, IConfiguration configuration)
{
var settings = configuration.GetSection("WeatherClient").Get<WeatherSettings>();
services.AddRefitClient<IWeatherClient>().ConfigureHttpClient((sp, client) =>
{
client.BaseAddress = new Uri(settings.BaseAddress);
client.Timeout = TimeSpan.FromSeconds(settings.TimeoutSeconds);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
});
return services;
}
Then you just need to add AddWeatherClient(Configuration)
.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddWeatherClient(Configuration);
}
Lastly, we are going to call our API from our controller.
Normally I would create a service for this but keeping it simple for brevity. Our controller now looks like this:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IWeatherClient _weatherClient;
public WeatherForecastController(ILogger<WeatherForecastController> logger, IWeatherClient weatherClient)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_weatherClient = weatherClient ?? throw new ArgumentNullException(nameof(weatherClient));
}
[HttpGet]
public async Task<IActionResult> GetAsync(CancellationToken ct)
{
var weatherResponse = await _weatherClient.GetWeatherAsync(51.5074m, 0.1278m, ct);
if (!weatherResponse.IsSuccessStatusCode || weatherResponse?.Content?.Dataseries == null)
{
_logger.LogError("Unexpected status code from Weather API {StatusCode}", weatherResponse.StatusCode);
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
}
return Ok(weatherResponse.Content.Dataseries.Select(weather => new WeatherForecast
{
Date = DateTime.ParseExact(weather.Date.ToString(), "yyyyMMdd", CultureInfo.InvariantCulture),
TemperatureC = new Temperature { Min = weather.Temp2m.Min, Max = weather.Temp2m.Max },
Summary = weather.Weather
}));
}
}
Now when we run our API and call the get endpoint we will get back the results from the actual API.
[
{
"date": "2021-05-21T00:00:00",
"temperatureC": { "min": 9, "max": 11 },
"temperatureF": { "min": 48, "max": 51 },
"summary": "lightrain"
},
{
"date": "2021-05-22T00:00:00",
"temperatureC": { "min": 7, "max": 10 },
"temperatureF": { "min": 44, "max": 49 },
"summary": "lightrain"
},
{
"date": "2021-05-23T00:00:00",
"temperatureC": { "min": 4, "max": 12 },
"temperatureF": { "min": 39, "max": 53 },
"summary": "lightrain"
},
{
"date": "2021-05-24T00:00:00",
"temperatureC": { "min": 7, "max": 12 },
"temperatureF": { "min": 44, "max": 53 },
"summary": "rain"
},
{
"date": "2021-05-25T00:00:00",
"temperatureC": { "min": 7, "max": 12 },
"temperatureF": { "min": 44, "max": 53 },
"summary": "rain"
},
{
"date": "2021-05-26T00:00:00",
"temperatureC": { "min": 6, "max": 15 },
"temperatureF": { "min": 42, "max": 58 },
"summary": "rain"
},
{
"date": "2021-05-27T00:00:00",
"temperatureC": { "min": 6, "max": 18 },
"temperatureF": { "min": 42, "max": 64 },
"summary": "clear"
}
]
Because we are calling the actual API we can’t test what happens with a different response from the API. This is why we need to mock out this external dependency in our integration tests.
Creating our Integration Test Project
I prefer to have my projects laid out in the following format:
src/
├── Project/
│ ├── Project.csproj
│
test/
├── Project.Tests/
│ ├── Project.Tests.csproj
Project.sln
After moving the project into a separate src
folder we can then use the dotnet templates to create our xunit test project inside our test project folder.
dotnet new xunit
Lastly back in the route folder we are going to create our solution file and add our projects to it.
dotnet new sln
dotnet sln add src/*
dotnet sln add test/*
Next we need to add a few packages to our test project:
<PackageReference Include="WireMock.Net" Version="1.4.15"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6"/>
<PackageReference Include="FluentAssertions" Version="5.10.3"/>
We also need to add a reference to our main project as well as an appsettings.test.json
file and a Resources
folder that we will add in a moment. Your test project should look something like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp5.0</TargetFramework>
<RootNamespace>WireMock.Net.Test</RootNamespace>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="5.0.6"/>
<PackageReference Include="xunit" Version="2.4.0"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0"/>
<PackageReference Include="coverlet.collector" Version="1.2.0"/>
<PackageReference Include="WireMock.Net" Version="1.4.15"/>
<PackageReference Include="FluentAssertions" Version="5.10.3"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\WireMock.Net.Api\WireMock.Net.Api.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.test.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<Content Include="Resources\*.*">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
Now go and create an appsettings.test.json
file and a Resources
folder. The Resources folder is where we are going to put the mocked responses for our API.
In your appsettings.test.json
file put the following:
{
"WeatherClient": {
"BaseAddress": "http://localhost:50000"
}
}
We will set up our wiremock on this port in a bit.
Set up the WebApplicationFactory
To be able to replace the call to our actual API with our WireMock version we need our API to pick up our appsettings.test.json
file so that we can replace the BaseAddress
. To do this we are going to create a WebApplicationFactory and use it as a Class fixture in our tests.
using System.IO;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using WireMock.Net.Api;
namespace WireMock.Net.Test.Infrastructure
{
public class ApiWebFactory<TStartup> : WebApplicationFactory<TStartup> where TStartup : class
{
protected override IWebHostBuilder CreateWebHostBuilder() =>
WebHost.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
config
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile("appsettings.test.json", false, false);
})
.UseContentRoot(Directory.GetCurrentDirectory())
.UseKestrel()
.UseStartup<Startup>();
}
}
Next, we are going to create a base class for our integration tests to simplify making calls to our API and reading the responses.
Note: we need to add an XUnit Collection so that the tests are run sequentially, otherwise we are going to have issues with conflicting port numbers when running our test.
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Testing;
using Newtonsoft.Json;
using WireMock.Net.Api;
using Xunit;
namespace WireMock.Net.Test.Infrastructure
{
[Collection("sequential")]
public abstract class IntegrationBase : IClassFixture<ApiWebFactory<Startup>>
{
protected HttpClient HttpClient { get; }
protected IntegrationBase(WebApplicationFactory<Startup> factory)
{
HttpClient = factory.CreateClient();
factory.Server.AllowSynchronousIO = true;
}
protected HttpRequestMessage CreateGetRequest(string url)
{
return new HttpRequestMessage(HttpMethod.Get, url);
}
protected static async Task<T> ReadResponseAsync<T>(HttpResponseMessage response)
{
var result = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<T>(result);
}
}
}
Then we need to create our mock API for the weather endpoint.
using System;
using System.IO;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
namespace WireMock.Net.Test.Infrastructure
{
public class WeatherFixture : IDisposable
{
protected readonly WireMockServer _mockApi;
public WeatherFixture()
{
_mockApi = WireMockServer.Start(50000);
}
public void Dispose()
{
_mockApi.Stop();
}
public void Reset()
{
_mockApi.Reset();
}
public IRequestBuilder SetupGetWeather(string responseBodyResource, int statusCode = 200)
{
var request = Request.Create()
.UsingGet()
.WithPath("/bin/civillight.php*");
var responseBody = string.IsNullOrWhiteSpace(responseBodyResource) ? new byte[0] : File.ReadAllBytes(responseBodyResource);
_mockApi.Given(request)
.RespondWith(
Response.Create()
.WithStatusCode(statusCode)
.WithHeader("content-type", "application/json")
.WithBody(responseBody)
);
return request;
}
}
}
In this class, we are starting the WireMockServer on port 50000. This is so that we know that our mock API is going to be running on localhost:50000
when we run our tests.
The SetupGetWeather
method is used to set up the expected request and response. In our case, anything that matches /bin/civillight.php
at the start is going to be mocked.
Then have the response, which is retrieved from a file and returned in the body. Remember that Resources
folder we created earlier? That is where we are going to put our JSON responses.
Create a file called success.json
in the Resources
folder and put the successful response from the weather API in there.
{
"product": "civillight",
"init": "2021052100",
"dataseries": [
{
"date": 20210521,
"weather": "lightrain",
"temp2m": { "max": 11, "min": 9 },
"wind10m_max": 4
},
{
"date": 20210522,
"weather": "lightrain",
"temp2m": { "max": 10, "min": 7 },
"wind10m_max": 3
},
{
"date": 20210523,
"weather": "lightrain",
"temp2m": { "max": 12, "min": 4 },
"wind10m_max": 4
},
{
"date": 20210524,
"weather": "rain",
"temp2m": { "max": 12, "min": 7 },
"wind10m_max": 3
},
{
"date": 20210525,
"weather": "rain",
"temp2m": { "max": 12, "min": 7 },
"wind10m_max": 3
},
{
"date": 20210526,
"weather": "rain",
"temp2m": { "max": 15, "min": 6 },
"wind10m_max": 3
},
{
"date": 20210527,
"weather": "clear",
"temp2m": { "max": 18, "min": 6 },
"wind10m_max": 3
}
]
}
Lastly we create our Integration test and make sure we get back the expected response:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using FluentAssertions;
using WireMock.Net.Api;
using WireMock.Net.Test.Infrastructure;
using Xunit;
namespace WireMock.Net.Test
{
public class IntegrationTest : IntegrationBase, IDisposable
{
private readonly WeatherFixture _weatherFixture;
public IntegrationTest(ApiWebFactory<Startup> factory) : base(factory)
{
_weatherFixture = new WeatherFixture();
}
public void Dispose()
{
_weatherFixture.Reset();
_weatherFixture.Dispose();
}
[Fact]
public async Task Given_weather_api_successful_returns_weather()
{
// Arrange
_weatherFixture.SetupGetWeather("Resources/success.json");
// Act
var request = CreateGetRequest("/weatherforecast");
var result = await HttpClient.SendAsync(request);
// Assert
result.StatusCode.Should().Be(HttpStatusCode.OK);
var response = await ReadResponseAsync<IEnumerable<WeatherForecast>>(result);
response.Should().HaveCount(7);
response.Should().Contain(x =>
x.Date == DateTime.Parse("2021-05-25") &&
x.Summary == "rain" &&
x.TemperatureC.Max == 12 &&
x.TemperatureC.Min == 7);
}
[Fact]
public async Task Given_weather_api_unsuccessful_returns_500()
{
// Arrange
_weatherFixture.SetupGetWeather(null, 503);
// Act
var request = CreateGetRequest("/weatherforecast");
var result = await HttpClient.SendAsync(request);
// Assert
result.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
}
}
}
Now run the tests using dotnet test
and you should see that your API is calling the mocked endpoint.
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[100]
Start processing HTTP request GET http://localhost:50000/bin/civillight.php?tzshift=0&output=json&unit=metric&ac=0&lon=0.1278&lat=51.5074
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[100]
Sending HTTP request GET http://localhost:50000/bin/civillight.php?tzshift=0&output=json&unit=metric&ac=0&lon=0.1278&lat=51.5074
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[101]
Received HTTP response headers after 107.6376ms - 200
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[101]
End processing HTTP request after 117.0377ms - 200
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[100]
Start processing HTTP request GET http://localhost:50000/bin/civillight.php?tzshift=0&output=json&unit=metric&ac=0&lon=0.1278&lat=51.5074
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[100]
Sending HTTP request GET http://localhost:50000/bin/civillight.php?tzshift=0&output=json&unit=metric&ac=0&lon=0.1278&lat=51.5074
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[101]
Received HTTP response headers after 13.8969ms - 503
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+WireMockNetApiClientIWeatherClient, WireMock.Net.Api, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[101]
End processing HTTP request after 14.0039ms - 503
fail: WireMock.Net.Api.Controllers.WeatherForecastController[0]
Unexpected status code from Weather API ServiceUnavailable
Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2, Duration: 832 ms
Final Comments
As I showed in my last post you can easily mock an API using WireMock in a docker container. However, if you want to be able to test different responses using the same endpoint then, you need to set up Wiremock.net directly in your integration tests.
You can find the complete code for this tutorial on my GitHub page.
If you have any questions let me know in the comments below.