Integration tests without API dependencies with ASP.NET Core and WireMock.Net
Daniel Genezini
Posted on December 20, 2022
Introduction
Although there are many definitions about the scope of an integration test, Martin Fowler defines Narrow integration tests, where the integration with other systems are tested using mocks, and Broad integration tests, where they communicate using real APIs.
In this post, I'll explain how to create mocks for HTTP APIs in narrow integration tests using the WireMock.Net library.
What should we mock?
Vladimir Khorikov has a concept of managed dependencies and unmanaged dependencies, which I consider complementary to Martin Fowler's, to choose what should be to mocked.
Managed dependencies are external systems controlled by us and accessed only by our application (for example, a database). On the other side, unmanaged dependencies are external systems not controlled by us or also accessed by other applications (like a third party API or a message broker).
Vladimir says that we should test our system against managed dependencies, while mocking unmanaged dependencies. I believe this definition is more like a guideline than a rule. For example, in a scenario where our application posts in a message broker for other system to read, that is, the message broker is an unmanaged dependency, we could test the integration with the message broker to validate that the message is being written in the right format (according to contract). This can have value if we want to test if updates to the library used to communicate with the message broker didn't introduce breaking changes in the message.
Why use mocks?
The reason we use integration tests is to test our components (or classes), which are tested independently in unit tests, working in communication with each other. When we interact with an API, we follow a protocol and trust a contract of communication, that is, that the API will accept parameters X as input and will return an response Y.
That way, the inner works of that external API is not in the scope of our integration tests.
This doesn't remove the requirement of functional tests; it only reduces the amount of those tests, which are more expensive to execute.
Reducing the integration tests only to our application, we have some benefits:
- Speed of the tests, because we remove the network latency;
- No need of data in external systems to execute the tests;
- Reduced brittleness of the tests, that could break in case of the external API instability or external data that changed;
- More trust in the test results.
Using WireMock.Net
In this example, I've built an API that consumes the PokéAPI service to look for a Pokémon data and return it to the client.
Controller
The controller is simple and use the Refit library to abstract the PokéAPI call and then, returns the data.
using Microsoft.AspNetCore.Mvc;
using Refit;
namespace PokemonInfoAPI.Controllers
{
[ApiController]
[Route("[controller]")]
public class PokemonInfoController : ControllerBase
{
private readonly IConfiguration _configuration;
public PokemonInfoController(IConfiguration configuration)
{
_configuration = configuration;
}
[HttpGet("{pokemonName}")]
public async Task<ActionResult<PokemonInfo>> GetAsync(string pokemonName)
{
try
{
var pokeApi = RestService.For<IPokeApi>(_configuration["PokeApiBaseUrl"]);
return Ok(await pokeApi.GetPokemonInfo(pokemonName));
}
catch (ApiException ex)
{
if (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return NotFound();
}
return StatusCode(500);
}
}
}
}
Default integration test
We start with a default integration test, using ASP.NET Core's WebApplicationFactory class.
The test creates an instance of our application e makes a request to the /pokemoninfo
endpoint with the parameter charmander
. For now, our test will call the PokéAPI.
💡 You can use any class of your API project to instanciate the
WebApplicationFactory
in your tests. If you're using top-level statements in your application, you can use a controller class. For example,WebApplicationFactory<PokemonInfoController>
.
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using System.Net;
using System.Text.Json;
namespace PokemonInfoAPI.IntegrationTests
{
public class PokemonInfoTests: IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public PokemonInfoTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task Get_Existing_Pokemon_Returns_200()
{
//Arrange
var HttpClient = Factory.CreateClient();
//Act
var HttpResponse = await HttpClient.GetAsync("/pokemoninfo/charmander");
//Assert
HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var ResponseJson = await HttpResponse.Content.ReadAsStringAsync();
var PokemonInfo = JsonSerializer.Deserialize<PokemonInfo>(ResponseJson);
PokemonInfo.Should().BeEquivalentTo(ResponseObj);
}
}
}
Setting up a mock for PokéAPI
WireMock.Net is a library that let you create mocks for HTTP APIs. It creates a web server in the same process of our test and exposes an URL to be used by out application during the tests.
Using WireMock.Net and WebApplicationFactory we will have this scenario:
First, I install the WireMock.Net
nuget package in my tests project.
Using Visual Studio Package Manager
Install-Package WireMock.Net
Or
Using .NET CLI
dotnet add package WireMock.Net
Starting WireMock.Net server
To start the WireMock.Net server, I call the Start
method of the WireMockServer
class, and it returns an object with the server data.
var WireMockSvr = WireMockServer.Start();
Overriding out application configurations
With the server started, I override the PokeApiBaseUrl
parameter, which holds the PokéAPI URL, in my application configurations using the method WithWebHostBuilder
of the WebApplicationFactory
:
var HttpClient = _factory
.WithWebHostBuilder(builder =>
{
builder.UseSetting("PokeApiBaseUrl", WireMockSvr.Url);
})
.CreateClient();
Mocking the /pokemon endpoint
Then, I create the mock for the /pokemon
endpoint receiving the parameter value charmander
.
In the example below, I'm using the AutoFixture library to generate an object with random values, that will be returned by the mocked API.
ℹ️ By using an object, I can compare the return of my application with this object, but it's also possible to configure the return based on an file with a JSON, with the
WithBodyFromFile
method.
Also, I set the headers that will be returned and the HTTP status of the response.
Fixture fixture = new Fixture();
var ResponseObj = fixture.Create<PokemonInfo>();
var ResponseObjJson = JsonSerializer.Serialize(ResponseObj);
WireMockSvr
.Given(Request.Create()
.WithPath("/pokemon/charmander")
.UsingGet())
.RespondWith(Response.Create()
.WithBody(ResponseObjJson)
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.OK));
After that, my application inside the tests will be using the mocked version of the PokéAPI.
Complete test code
[Fact]
public async Task Get_Existing_Pokemon_Returns_200()
{
//Arrange
var WireMockSvr = WireMockServer.Start();
var HttpClient = _factory
.WithWebHostBuilder(builder =>
{
builder.UseSetting("PokeApiBaseUrl", WireMockSvr.Url);
})
.CreateClient();
Fixture fixture = new Fixture();
var ResponseObj = fixture.Create<PokemonInfo>();
var ResponseObjJson = JsonSerializer.Serialize(ResponseObj);
WireMockSvr
.Given(Request.Create()
.WithPath("/pokemon/charmander")
.UsingGet())
.RespondWith(Response.Create()
.WithBody(ResponseObjJson)
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.OK));
//Act
var HttpResponse = await HttpClient.GetAsync("/pokemoninfo/charmander");
//Assert
HttpResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var ResponseJson = await HttpResponse.Content.ReadAsStringAsync();
var PokemonInfo = JsonSerializer.Deserialize<PokemonInfo>(ResponseJson);
PokemonInfo.Should().BeEquivalentTo(ResponseObj);
WireMockSvr.Stop();
}
Example of an unsuccessfull API call scenario
Based on the contract of the API, we know that it return the status 404 (Not Found) when the parameter is not a valid Pokémon name, so I created a mock that returns this status for the parameter value woodywoodpecker
and assert that my application response is correct for this scenario.
[Fact]
public async Task Get_NotExisting_Pokemon_Returns_404()
{
//Arrange
var WireMockSvr = WireMockServer.Start();
var Factory = _factory.WithWebHostBuilder(builder =>
{
builder.UseSetting("PokeApiBaseUrl", WireMockSvr.Url);
});
var HttpClient = Factory.CreateClient();
Fixture fixture = new Fixture();
WireMockSvr
.Given(Request.Create()
.WithPath("/pokemon/woodywoodpecker")
.UsingGet())
.RespondWith(Response.Create()
.WithHeader("Content-Type", "application/json")
.WithStatusCode(HttpStatusCode.NotFound));
//Act
var HttpResponse = await HttpClient
.GetAsync("/pokemoninfo/woodywoodpecker");
//Assert
HttpResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
WireMockSvr.Stop();
}
Source code
https://github.com/dgenezini/PokemonInfoAPIWireMockTests
Liked this post?
I post extra content in my personal blog. Click here to see.
Follow me
References and links
- https://martinfowler.com/bliki/IntegrationTest.html
- https://khorikov.org/posts/2021-11-29-unmanaged-dependencies-explained/
- https://github.com/WireMock-Net/WireMock.Net
- https://github.com/reactiveui/refit
- https://github.com/AutoFixture/AutoFixture
- https://github.com/fluentassertions/fluentassertions
Posted on December 20, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
December 20, 2022