This article will cover all the bases when it comes to creating and managing mock APIs for integration testing with WireMock.NET.
Let’s jump in!
What Is Integration Testing and Why Is It Important?
Integration testing ensures smooth communication between different components of a software system, helping eliminate any issues that may arise. It plays a crucial role in testing how our applications interact with both internal and external APIs, confirming compliance with predefined contracts and desired functionality.
WireMock.NET is a comprehensive package that mimics HTTP API behavior, making it ideal for simulating APIs in various scenarios, such as testing classes dependent on APIs or performing integration tests with external APIs.
For a detailed exploration of integration testing in ASP.NET Core check out our article Integration Testing in ASP.NET Core.
Setting up Our Integration Testing Example
Imagine we are tasked with building a service that interacts with an API that is still in development. We are given only an endpoint – planets/{id}
and a contract class:
public class Planet { public int Id { get; set; } public string Name { get; set; } public double Diameter { get; set; } public int NumberOfMoons { get; set; } public bool HasAtmosphere { get; set; } public Planet(int id, string name, double diameter, int numberOfMoons, bool hasAtmosphere) { Id = id; Name = name; Diameter = diameter; NumberOfMoons = numberOfMoons; HasAtmosphere = hasAtmosphere; } }
The contract defines a Planet
class with properties representing the Id
, Name
, Diameter
, NumberOfMoons
, and HasAtmosphere
for a given planet. It also includes a constructor to initialize these properties when creating a new Planet
object.
Having this information we create the service:
public class PlanetsService { private readonly HttpClient _httpClient; public PlanetsService(HttpClient httpClient) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); } public async Task<Planet?> GetPlanetByIdAsync(int id) { var response = await _httpClient.GetAsync($"planets/{id}"); if (response.StatusCode == HttpStatusCode.OK) { return JsonSerializer.Deserialize<Planet>(await response.Content.ReadAsStringAsync()); } return null; } }
The PlanetsService
class has one GetPlanetByIdAsync
method that uses an HttpClient
to send an HTTP GET
request to retrieve information about a planet with a specified ID
. It returns the deserialized Planet
object if the request is successful, or null otherwise.
In the next sections, we will see how WireMock.NET can help us with the testing of our service.
Installing WireMock.NET
First, we need to install the WireMock.Net NuGet package.
Let’s do this via the .NET CLI:
dotnet add package WireMock.Net
Once this is done, we can look and how the package works and how we can use it.
Understanding How WireMock.NET Works
WireMock.NET is a powerful tool that can accurately emulate the behavior of an HTTP API by capturing incoming requests and directing them to a WireMock.NET HTTP server. This capability grants us the ability to define expectations, call the API, and verify its behavior, making it ideal for extensive testing of code interactions with external services.
With WireMock.NET, we can effectively mock real API endpoints and utilize its comprehensive features for HTTP response stubbing, including matching stubs based on URL/Path, headers, cookies, and body content patterns.
Creating Mock APIs for Integration Testing With WireMock.NET
To test our service we need a test project, and for this, we opt for xUnit:
public class PlanetsServiceTests : IDisposable { private readonly HttpClient _httpClient; private readonly WireMockServer _mockServer; private readonly PlanetsService _planetService; public PlanetsServiceTests() { _mockServer = WireMockServer.Start(); _httpClient = new HttpClient { BaseAddress = new Uri(_mockServer.Urls[0]) }; _planetService = new PlanetsService(_httpClient); } public void Dispose() { _mockServer.Stop(); _httpClient.Dispose(); } }
We define a PlanetsServiceTests
class that implements the IDisposable
interface. We set up an instance of a mock server using the WireMockServer.Start
method. Next, we create an instance of HttpClient
with the base address set to the first URL of our _mockServer
field. We move on to initialize an instance of PlanetsService
passing the _httpClient
as a parameter. Finally, we implement the Dispose
method to stop the mock server and dispose of the HttpClient
when the instance is no longer needed.
Mocking an HTTP Request and an OK Response With WireMock.NET
We can use WireMock.NET to simulate a successful HTTP response and body from a server. To that, we create a test that verifies the behavior of the GetPlanetByIdAsync
method in the PlanetsService
class.
First, we set up the necessary arrangements and conditions:
var planet = new Planet(4, "Mars", 6779, 2, true);
We create a Planet
object representing the planet Mars.
Next, we configure an instance of the WireMockServer
:
_mockServer .Given( Request.Create() .UsingGet() .WithPath("/planets/4")) .RespondWith( Response.Create() .WithStatusCode(HttpStatusCode.OK) .WithBodyAsJson(planet));
In the Given
method we start with creating the request we expect. To do this, we use the Request.Create
method. Then we state that we expect a GET
request to the /planets/4
endpoint. Then in the RespondWith
of the _mockServer
instance we specify the response to be returned. In this case, it responds with a JSON representation of the planet object and a status code of HttpStatusCode.OK
indicating that the response was a success.
After the arrangement, we proceed with the Act part of our test:
var result = await _planetService.GetPlanetByIdAsync(planet.Id);
We call the GetPlanetByIdAsync
method of the _planetService
instance, passing in the ID of the planet object created earlier.
Finally, we assert the expected behavior and examine the final method:
[Fact] public async Task GivenThatPlanetExists_WhenGetPlanetByIdIsInvoked_ThenValidPlanetIsReturned() { // Arrange var planet = new Planet(4, "Mars", 6779, 2, true); _mockServer .Given( Request.Create() .UsingGet() .WithPath("/planets/4")) .RespondWith( Response.Create() .WithStatusCode(HttpStatusCode.OK) .WithBodyAsJson(planet)); // Act var result = await _planetService.GetPlanetByIdAsync(planet.Id); // Assert result.Should().NotBeNull(); result.Name.Should().Be(planet.Name); result.Diameter.Should().Be(planet.Diameter); result.NumberOfMoons.Should().Be(planet.NumberOfMoons); result.HasAtmosphere.Should().Be(planet.HasAtmosphere); }
We do the assertions using the FluentAssertions library and check that everything with the returned Planet
object is identical to what we expect to get.
Mocking an HTTP Request and a NotFound Response With WireMock.NET
WireMock.NET can be used to mock other HTTP responses as well:
[Fact] public async Task GivenThatPlanetDoesntExists_WhenGetPlanetIsInvoked_ThenNullIsReturned() { // Arrange _mockServer .Given( Request.Create() .UsingGet() .WithPath("/planets/9")) .RespondWith( Response.Create() .WithStatusCode(HttpStatusCode.NotFound)); // Act var result = await _planetService.GetPlanetByIdAsync(9); // Assert result.Should().BeNull(); }
We start by configuring the _mockServer
. We state that we expect a GET
request to the path /planets/9
(representing the ID
of a non-existent planet). Then, we specify the response that we want the server to return. Here, the response is set to have a status code of HttpStatusCode.NotFound
, indicating that the requested resource was not found. After this, we move on to call the GetPlanetByIdAsync
method of the _planetService
instance. This time we pass in an ID
of a planet that doesn’t exist. Finally, we verify that the returned result should be null
.
Different Ways to Enhance Integration Tests With WireMock.NET
WireMock.NET has a variety of settings and methods that can help us enhance our integration tests. The package also provides us with ample ways to fine-tune our mock server in various ways.Â
Defining Custom URLs and SSL for the WireMock.NET Server
We have to possibility to initialize the client with custom URLs:
var server = WireMockServer.Start(new WireMockServerSettings { Urls = new[] { "http://localhost:7777/", "http://localhost:8888/", "http://localhost:9999/" }, UseSSL = true });
We initialize and start a WireMockServer
instance with specific URL settings. The server will be listening on three different URLs: http://localhost:7777/
, http://localhost:8888/
, and http://localhost:9999/
. These URLs represent the endpoints where the mock server will simulate the behavior of external services or dependencies during our integration testing. Additionally, we enable SSL (Secure Sockets Layer) by setting the UseSSL
property to true
, ensuring that the server supports secure communication over HTTPS.
Delayed Responses For Integration Testing With WireMock.NET
Like any real-life API, we can configure delays for our mock server:
server .Given( Request.Create() .UsingGet() .WithPath("/planets/9")) .RespondWith( Response.Create() .WithStatusCode(HttpStatusCode.NotFound) .WithDelay(TimeSpan.FromMilliseconds(500)));
Using our configuration from the HttpStatusCode.NotFound
example, we update the response by adding a half-second delay. To do this we use the WithDelay
method and pass TimeSpan.FromMilliseconds(500)
.
We can also have a random delay:
server .Given( Request.Create() .UsingGet() .WithPath("/planets/9")) .RespondWith( Response.Create() .WithStatusCode(HttpStatusCode.NotFound) .WithRandomDelay());
WireMock.NET provides the WithRandomDelay
method. By default, it will delay the response with a value between 0 and 60000 milliseconds, but we can pass our preferences as well.
Simulating Faults For Integration Testing With WireMock.NET
One key benefit of WireMock.NET it allows us to test the stability of your system against faulty or unforeseen behavior. One way to do this is by returning faulty responses:
server .Given( Request.Create() .UsingGet() .WithPath("/planets/3")) .RespondWith( Response.Create() .WithFault(FaultType.MALFORMED_RESPONSE_CHUNK));
We set the server to expect a GET
request with the path /planets/3
and responds with a fault of type MALFORMED_RESPONSE_CHUNK
. With this configuration we simulate a scenario where the server intentionally sends a response with a malformed response chunk, allowing testing of error handling or resilience mechanisms in our code. We have one more fault type at our disposal – EMPTY_RESPONSE
. Just as the name suggests, this will return an empty response from the server.
To take this one step further, the WithFault
method has an optional percentage parameter of double
type. By passing a value between 0 and 1 we set how likely is that the server will return a faulty response, gaining a level of randomness.
Custom Header, Wildcards, and Priority Request Matching for Integration Testing With WireMock.NET
We can set up our server to respond only if a particular header is present:
server .Given( Request.Create() .UsingGet() .WithPath("/planets/4") .WithHeader("AwesomeRedHeader", "MarsIsAwesome")) .RespondWith( Response.Create() .WithSuccess() .WithBody("The Red Planet is responding to the awesome header"));
We specify that when a GET
request is made to the path /planets/4
with a header AwesomeRedHeader
having the value MarsIsAwesome
, the server should respond with a successful status code and a body containing the message The Red Planet is responding to the awesome header.Â
WireMock.NET also has functionality for wildcards and priority matching:
server .Given( Request.Create() .UsingPost() .WithPath("/planets/*")) .AtPriority(5) .RespondWith( Response.Create() .WithStatusCode(HttpStatusCode.Unauthorized)); server .Given( Request.Create() .UsingPost() .WithPath("/planets/10")) .AtPriority(1) .RespondWith( Response.Create() .WithStatusCode(HttpStatusCode.Created));
We set up two request mappings on the WireMockServer
instance. The first mapping is a POST
request to any path starting with /planets/
with a priority of 5, and it responds with an HTTP status code of Unauthorized. The second mapping is a more specific POST
request to the /planets/10
path with a higher priority of 1, and it responds with an HTTP status code of Created. The priority allows the server to prioritize matching the more specific mapping over the general one when multiple mappings match the incoming request.
Conclusion
In conclusion, WireMock.NET offers numerous benefits for effective integration testing. It enables the emulation of HTTP APIs, allowing the capture and direction of incoming requests to a server created by WireMock.NET. This capability empowers developers to define expectations, call the API, and verify its behavior.
WireMock.NET provides extensive features for HTTP response stubbing and testing, including matching stubs based on various criteria. WireMock.NET offers a robust and customizable solution for API mocking and testing.
It’s highly recommended to give WireMock.NET a try for integration testing needs, as it provides a comprehensive toolset for simulating API behavior and validating interactions.