Integration tests without API dependencies with ASP.NET Core and WireMock.Net

Daniel Genezini - Dec 20 '22 - - Dev Community

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.

Diagram of the API

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);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

Diagram of an integration test with WireMock.Net and WebApplicationFactory

First, I install the WireMock.Net nuget package in my tests project.

Using Visual Studio Package Manager

Install-Package WireMock.Net
Enter fullscreen mode Exit fullscreen mode

Or

Using .NET CLI

dotnet add package WireMock.Net
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . .