WireMock for dotnet core Integration tests

In many cases, to optimize or improve the system solution, the business decides to integrate with external systems. External systems have their life cycle, state, and communication protocol. There are different types of communication protocols, in this article, we will only consider HTTP. Microservices also use synchronous HTTP calls for notification or to achieve strong consistency.

You definitely want your integration to have fewer problems and be able to diagnose them easily. In order to implement reliable communication between the internal solution and the external system, it is necessary to cover the communication protocol with reliable integration tests.

Integration tests

There are several options for how to implement integration tests with an external system:

  • Use API calls to an external system during the tests
  • Unit testing
  • Mock of API of an external system over the network

The use of the API of an external system in integration tests has many limitations and disadvantages, such as:

  • Increases test execution time, remote HTTP call
  • Increases test fragility, temporary service unavailability, configuration change
  • The external system has a state that used by other tests or users
  • Additional costs for the service of an external system
  • Various limitations of the external system that do not allow setting up the test environment

Note

All these disadvantages apply only to integration tests. At the same time, they are an advantage for e2e tests, in which coordination with an external system is necessary. Typically, such e2e tests run on separate environments that do not overlap with users.

Unit testing is very fast but can hide some of the nuances used in the HTTP protocol e.g. headers, cookies, etc.

Simulating the API of an external system via a network eliminates the disadvantages of those approaches. Also, it gives the best result from both worlds, the speed from the unit tests and coverage from the API call. As of this writing, there are two of the most well-known testing frameworks

The Pact is a more advanced consumer-driven framework that allows you to record consumer behaviors and play back scripts on the server-side. But in this article, we will be using WireMock to test the protocol between two services.

Demo example

For the demo purpose, we have a couple of services CatalogueService and InventoryService:

  • CatalogueService is responsible for the list of the products and it’s detailed descriptions.
  • InventoryService is responsible for product stock. It can set and get quantity values for the products.

CatalogueService clients would like to fetch all product and stock quantities via get request. To provide complete data CatalogueService needs to call InventoryService to enrich stock quantities for corresponding products. This way client will receive a complete product catalogue with inventory quantities.

Search Parameters

CatalogueService

This service provides v1/catalogue/products implemented in the ProductsCatalogueController.

ProductsCatalogueController is using CatalogueProvider to return products catalogue with stock quantities:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    [ApiController]
    [Route("v1/catalogue/products")]
    public class ProductsCatalogueController : ControllerBase
    {
        private readonly ICatalogueProvider _catalogueProvider;
        // ...
        [HttpGet]
        public async Task<IActionResult> GetProducts()
        {
            var catalogue = await _catalogueProvider.GetAllEntriesAsync();
            return Ok(catalogue);
        }
    }

CatalogueProvider retrieves inventory for each product from the ProductsRepository:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    public class CatalogueProvider : ICatalogueProvider
    {
        // ...
        public async Task<IEnumerable<CatalogueEntry>> GetAllEntriesAsync()
        {
            var products = _productsRepository.GetProducts();
            var httpClient = _httpClientFactory.CreateClient();
            var tasks = products.Select(product => GetCatalogueEntryAsync(_settings.InventoryServiceUrl, product, httpClient));

            return await Task.WhenAll(tasks);
        }

        private static async Task<CatalogueEntry> GetCatalogueEntryAsync(string inventoryServiceUrl, Product product, HttpClient httpClient)
        {
            var url = $"{inventoryServiceUrl}/v1/inventory/{product.ProductId}";
            var response = await httpClient.GetAsync(url);
            response.EnsureSuccessStatusCode();
            var inventory = await response.Content.ReadFromJsonAsync<GetInventoryResponse>();

            return new CatalogueEntry
            {
                ProductId = product.ProductId,
                Description = product.Description,
                Quantity = inventory.Quantity
            };
        }
    }

InventoryService

It is quite a simple service that exposes get and set endpoints to update in-memory repository:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    public class InventoryRepository : IInventoryRepository
    {
        private Dictionary<string, int> _data = new();
        
        public void SetProductQuantity(string productId, int quantity)
        {
            _data[productId] = quantity;
        }

        public int GetProductQuantity(string productId)
        {
            return _data.TryGetValue(productId, out var currentQuantity) ? currentQuantity : 0;
        }
    }

Configure Integration test

For integration testing, we need to perform some API calls and validate the response from the CatalogueService. Microsoft.AspNetCore.Mvc.Testing package contains WebApplicationFactory which allows us to create http client for our service. You can read more about WebApplicationFactory at the Integration tests in ASP.NET Core.

WebApplicationTest creates an instance of the CatalogueService and allows to create a http client for it to perform API calls:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    public class WebApplicationTest : WebApplicationFactory<Startup>
    {
        public ITestOutputHelper? TestOutputHelper { get; set; }

        protected override IHost CreateHost(IHostBuilder builder)
        {
            InventoryServiceMock = InventoryServiceMock.Start();

            builder.ConfigureLogging((_, logging) =>
            {
                logging.ClearProviders();
                logging.AddXUnit(TestOutputHelper);
            });

            builder.ConfigureAppConfiguration((_, configurationBuilder) => 
                ConfigureBuilder(configurationBuilder));
 
            var host = builder.Build();
            host.Start();
            return host;
        }
        
        private void ConfigureBuilder(IConfigurationBuilder builder)
        {
            builder.SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables();
        }
    }

Test collection allows to share WebApplicationTest between multiple tests:

1
2
3
4
    [CollectionDefinition(nameof(WebApplicationTestCollection))]
    public class WebApplicationTestCollection : ICollectionFixture<WebApplicationTest>
    {
    }

ProductsCatalogueTests using WebApplicationTest to create a clint with _factory.CreateClient and perfrom HTTP call to CatalogueService:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    [Collection(nameof(WebApplicationTestCollection))]
    public class ProductsCatalogueTests
    {
        private readonly WebApplicationTest _factory;

        public ProductsCatalogueTests(ITestOutputHelper testOutputHelper, WebApplicationTest factory)
        {
            _factory = factory;
            _factory.TestOutputHelper = testOutputHelper;
        }

        [Fact]
        private async Task ShouldReturnExpectedResult()
        {
            // Arrange
            var client = _factory.CreateClient();

            // Act
            var response = await client.GetAsync("v1/catalogue/products");

            // Assert
            response.StatusCode.Should().Be(HttpStatusCode.OK);
            var resultCatalogue = await response.Content.ReadFromJsonAsync<IEnumerable<CatalogueEntry>>();

            _factory.TestOutputHelper.WriteLine(JsonSerializer.Serialize(resultCatalogue));

            var expectedCatalogue = new CatalogueEntry[]
            {
                new()
                {
                    ProductId = "yellow-wings",
                    Description = "Yellow wings",
                    Quantity = 10
                },
                new()
                {
                    ProductId = "blue-wings",
                    Description = "Blue wings",
                    Quantity = 20
                },
                new()
                {
                    ProductId = "white-wings",
                    Description = "White wings",
                    Quantity = 30
                },
            };

            resultCatalogue.Should().BeEquivalentTo(expectedCatalogue);
        }
    }

This setup performs HTTP calls to the InventoryService. And to make CatalogueService's tests green, we need to run InventoryService and set correct stock values for products. It’s fine if we would like to test this behaviour manually but it doesn’t fit into the CI/CD pipeline. We don’t want to run InventoryService each time when we test CatalogueService, this will make our test complex and slows down delivery. WireMock to the rescue…

Configure WireMock.net

WireMock provides native API to create HTTP call matchers and return response body and code.

InventoryServiceMock is helper class for configuring mock service behaviour. E.g. by calling method Response_to_GetInventory("yellow-wings", 10) we instruct mock service to response with { "Quantity": 10 } if request path matches v1/inventory/yellow-wings.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public class InventoryServiceMock : WireMockServer
    {
        // ...
        public static InventoryServiceMock Start(int? port = 0, bool ssl = false)
        {
            return new (new WireMockServerSettings
            {
                Port = port,
                UseSSL = ssl
            });
        }
        
        public ExpectedResponse Response_to_GetInventory(string productId, int quantity)
        {
            var path = $"/v1/inventory/{productId}";
            
            Given(Request.Create().UsingGet()
                    .WithPath(path)
                )
                .RespondWith(Response.Create()
                    .WithBodyAsJson(new { Quantity = quantity })
                    .WithStatusCode(200));

            return new ExpectedResponse(this, path, 200);
        }
    }

To let CatalogueService call mock instead of InventoryService we need to create Inventory Mock service and update CatalogueService configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
    public class WebApplicationTest : WebApplicationFactory<Startup>
    {
        public ITestOutputHelper? TestOutputHelper { get; set; }
        public InventoryServiceMock? InventoryServiceMock { get; private set; }

        protected override IHost CreateHost(IHostBuilder builder)
        {
            // >>> Create Mock service
            InventoryServiceMock = InventoryServiceMock.Start();
            // >>> Create Mock service

            builder.ConfigureLogging((_, logging) =>
            {
                logging.ClearProviders();
                logging.AddXUnit(TestOutputHelper);
            });

            builder.ConfigureAppConfiguration((_, configurationBuilder) => 
                ConfigureBuilder(configurationBuilder));
 
            var host = builder.Build();
            host.Start();
            return host;
        }
        
        private void ConfigureBuilder(IConfigurationBuilder builder)
        {
            builder.SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .AddEnvironmentVariables()
                
                // >>> Update configuration
                .AddInMemoryCollection(new Dictionary<string, string>
                {
                    { "Settings:InventoryServiceUrl", $"{InventoryServiceMock.Urls.First()}" },
                });
                // >>>
        }
    }

And we need to update tests to instruct WireMock how to react to API calls:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    [Collection(nameof(WebApplicationTestCollection))]
    public class ProductsCatalogueTests
    {
        // ...
        [Fact]
        private async Task ShouldReturnExpectedResult()
        {
            // Arrange
            var client = _factory.CreateClient();

            // >>> Mock InventoryService behaviour
            var getYellowWingsInventory = _factory.InventoryServiceMock.Response_to_GetInventory("yellow-wings", 10);
            var getBlueWingsInventory = _factory.InventoryServiceMock.Response_to_GetInventory("blue-wings", 20);
            var getWhiteWingsInventory = _factory.InventoryServiceMock.Response_to_GetInventory("white-wings", 30);
            // >>>

            // Act
            var response = await client.GetAsync("v1/catalogue/products");

            // Assert
            response.StatusCode.Should().Be(HttpStatusCode.OK);
            var resultCatalogue = await response.Content.ReadFromJsonAsync<IEnumerable<CatalogueEntry>>();

            _factory.TestOutputHelper.WriteLine(JsonSerializer.Serialize(resultCatalogue));

            var expectedCatalogue = new CatalogueEntry[]
            {
                new()
                {
                    ProductId = "yellow-wings",
                    Description = "Yellow wings",
                    Quantity = 10
                },
                new()
                {
                    ProductId = "blue-wings",
                    Description = "Blue wings",
                    Quantity = 20
                },
                new()
                {
                    ProductId = "white-wings",
                    Description = "White wings",
                    Quantity = 30
                },
            };

            resultCatalogue.Should().BeEquivalentTo(expectedCatalogue);

            // >>> Assert that service was called
            getYellowWingsInventory.ShouldBeCompleted();
            getBlueWingsInventory.ShouldBeCompleted();
            getWhiteWingsInventory.ShouldBeCompleted();
            // >>> 
        }
    }

Results

The complete Integration test with WireMock example can be found in the Git repository blog-wiremock-integration-test.

This integration tests setup has the following advantages:

  • Run integration tests with the same speed as Unit tests
  • Inbound and outbound API calls executed over the HTTP protocol
  • Similar to the black box integration tests
  • Do not dependent on external systems, no flakiness, no dependency on environment state, etc

Happy testing!