How To Test Integrations with APIs Using WireMock in .NET

antonmartyniuk

Anton Martyniuk

Posted on October 1, 2024

How To Test Integrations with APIs Using WireMock in .NET

Integration testing is a type of software testing essential for validating the interactions between different components of an application, ensuring they work together as expected.
The main goal of integration testing is to identify any issues that may arise when these components interact with each other.

These component are: databases, caches, message queues, APIs, external services, etc.

In this blog post, we will focus on how to write integration tests that integrate with APIs.
And how you can use WireMock to mock API integrations.

Earlier I had a fantastic post on integration testing best practices, make sure to check it out.

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.

Introduction to API Integration Testing

In integration tests, we are testing one particular application that integrates with other services.

We don't need to run other services, for example, in Docker, to test integration with each other, either with synchronous or asynchronous communication (events).
If we decide to do so - these are another kind of tests, named End-To-End (E2E) tests.
In E2E tests, we test an entire set of services that work together as a whole.

In integration tests, it is sufficient to test if our service is sending request and receiving response to an API endpoint with a given schema.

As we don't have other services running during integration testing - we need to somehow simulate these services.
And here is where a WireMock library comes into play. WireMock allows mocking the APIs your service depends on.

Before diving deep into WireMock, let's first explore the application we will be testing.

The Application We Will Be Testing

Today I'll show you how to write integration tests for a Shipping Service that is responsible for creating and updating shipments for purchased products.

ShippingService implements the following use cases, available through webapi:

1. Create Shipment: saves shipment details to the database.

2. Update Shipment Status: updates the status of a shipment in the database.

3. Get Shipment By Number: returns a shipment from a database by a number.

ShippingService has the following integrations:

  • Postgres database, using EF Core
  • Stock-Service, Carrier-Service, using HTTP

Each use case exposes a webapi endpoint implemented by ASP.NET Core Minimal APIs.
Each endpoint uses MediatR to publish a corresponding Command or Query to implement the use case.

Let's have a look at implementations of "Create Shipment" use case.
It implements the following flow:

  • checks if a Shipment for a given OrderId is already created
  • checks Stock-Service whether it has available number of Products
  • creates Shipment in the database
  • sends request to the Carrier-Service with shipment details

Let's explore the command handler Handle method that implements the given logic:

internal sealed class CreateShipmentCommandHandler(
    EfCoreDbContext context,
    IStockApi stockApi,
    ICarrierApi carrierApi,
    ILogger<CreateShipmentCommandHandler> logger)
    : IRequestHandler<CreateShipmentCommand, ErrorOr<ShipmentResponse>>
{
    public async Task<ErrorOr<ShipmentResponse>> Handle(
        CreateShipmentCommand request,
        CancellationToken cancellationToken)
    {
        var shipmentAlreadyExists = await context.Shipments.AnyAsync(x => x.OrderId == request.OrderId, cancellationToken);
        if (shipmentAlreadyExists)
        {
            logger.LogInformation("Shipment for order '{OrderId}' is already created", request.OrderId);
            return Error.Conflict($"Shipment for order '{request.OrderId}' is already created");
        }

        var products = request.Items.Select(x => new Product(x.Product, x.Quantity)).ToList();
        var stockResponse = await stockApi.CheckStockAsync(new CheckStockRequest(products));

        if (!stockResponse.IsSuccessStatusCode)
        {
            logger.LogInformation("Received error from stock-service: {ErrorMessage}", stockResponse.Error.Content);
            return Error.Validation("ProductsNotAvailableInStock", $"Received error from stock-service: '{stockResponse.Error.Content}'");
        }

        if (!stockResponse.Content.IsSuccess)
        {
            logger.LogInformation("Received error from stock-service: {ErrorMessage}", stockResponse.Content.ErrorMessage);
            return Error.Validation("ProductsNotAvailableInStock", $"Received error from stock-service: '{stockResponse.Content.ErrorMessage}'");
        }

        var shipmentNumber = new Faker().Commerce.Ean8();
        var shipment = request.MapToShipment(shipmentNumber);

        await context.Shipments.AddAsync(shipment, cancellationToken);
        await context.SaveChangesAsync(cancellationToken);

        logger.LogInformation("Created shipment: {@Shipment}", shipment);

        var carrierRequest = new CreateCarrierShipmentRequest(request.OrderId, request.Address, request.Carrier, request.ReceiverEmail, request.Items);
        await carrierApi.CreateShipmentAsync(carrierRequest);

        var response = shipment.MapToResponse();
        return response;
    }
}
Enter fullscreen mode Exit fullscreen mode

For communication with Stock-Service and Carrier-Service I use IStockApi and ICarrierApi correspondingly.
They are Refit API interfaces:

public interface IStockApi
{
    [Post("/api/stocks/check")]
    Task<ApiResponse<CheckStockResponse>> CheckStockAsync([Body] CheckStockRequest request);
}

public interface ICarrierApi
{
    [Post("/api/shipments")]
    Task CreateShipmentAsync([Body] CreateCarrierShipmentRequest request);
}
Enter fullscreen mode Exit fullscreen mode

I really love using Refit library for communication with other services via HTTP protocol.
This library provides an interface wrapper (with code generation) that wraps HttpClient using HttpClientFactory.

Here's how these Refit interfaces are registered in the DI container:

public static IServiceCollection AddExternalServices(
    this IServiceCollection services,
    IConfiguration configuration)
{
    var carrierApiBaseUrl = configuration["CarrierApi:BaseUrl"]!;
    var stockApiBaseUrl = configuration["StockApi:BaseUrl"]!;

    services.AddRefitClient<ICarrierApi>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri(carrierApiBaseUrl));

    services.AddRefitClient<IStockApi>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri(stockApiBaseUrl));

    return services;
}
Enter fullscreen mode Exit fullscreen mode

Simple, isn't it?

The provided code that communicates with Stock-Service and Carrier-Service is simplified and is NOT production ready.
Its main purpose is to show how you can communication with other services over HTTP and how to write integration tests for such a communication

Introduction to WireMock

As I explained earlier, for integration testing, you don't need to run all the services together.
You can use WireMock to mock the corresponding services APIs.

WireMock has a flexible API that allows to mock any type of HTTP requests.
This library can respond with different status code, has matching filters for requests and other features.

We have 2 options on how to work with WireMock:

  • use WireMock.Net Nuget package and create a console/webapi application with HTTP requests mocking setup
  • or we can run WireMock in Docker with all mocking in the JSON files.

We will focus on the 2nd option as it is very easy to set up without writing any extra code.

In this blog post I will showcase how to use WireMock with TestContainers.
In my integration testing best practices I explained why using TestContainers is such a great option.

In short TestContainers library offers an elegant and easy way to run the integration tests.
It allows you to run all your external services as docker containers, as you probably love doing in development.

You need to install the following Nuget packages in your project:

<PackageReference Include="Testcontainers" Version="3.9.0" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.9.0" />
<PackageReference Include="WireMock.Net.Testcontainers" Version="1.6.0" />
Enter fullscreen mode Exit fullscreen mode

WireMock.Net.Testcontainers is a package that allows to run your WireMock as a Docker container.

Let's set up WebApplicationFactory for our integration tests.
We need to start a Postgres and WireMock containers:

public class CustomWebApplicationFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
            .WithImage("postgres:latest")
            .WithDatabase("test")
            .WithUsername("admin")
            .WithPassword("admin")
            .Build();

    private readonly IContainer _wireMockContainer = new ContainerBuilder()
        .WithImage("wiremock/wiremock:3.7.0")
        .WithPortBinding(8080, true)
        .WithBindMount(GetAbsolutePath("mocks"), "/home/wiremock")
        .Build();

    private DbConnection _dbConnection = null!;
    private string _wireMockUrl = string.Empty;

    public HttpClient HttpClient { get; private set; } = null!;

    public async Task InitializeAsync()
    {
        await _dbContainer.StartAsync();

        _dbConnection = new NpgsqlConnection(_dbContainer.GetConnectionString());

        _wireMockUrl = $"http://127.0.0.1:{_wireMockContainer.GetMappedPublicPort(8080)}";

        HttpClient = CreateClient();

        await _dbConnection.OpenAsync();
    }

    public new async Task DisposeAsync()
    {
        await _dbContainer.DisposeAsync();
        await _dbConnection.DisposeAsync();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        Environment.SetEnvironmentVariable("ConnectionStrings:Postgres", _dbContainer.GetConnectionString());

        Environment.SetEnvironmentVariable("CarrierApi:BaseUrl", _wireMockUrl);
        Environment.SetEnvironmentVariable("StockApi:BaseUrl", _wireMockUrl);
    }

    private static string GetAbsolutePath(string relativePath)
    {
        var currentExecutablePath = AppDomain.CurrentDomain.BaseDirectory;
        var resultUri = new Uri(new Uri(currentExecutablePath), relativePath);
        return Path.GetFullPath(resultUri.AbsolutePath);
    }
}
Enter fullscreen mode Exit fullscreen mode

The key here is creating URL to the WireMock mock service:

_wireMockUrl = $"http://127.0.0.1:{_wireMockContainer.GetMappedPublicPort(8080)}";

Environment.SetEnvironmentVariable("CarrierApi:BaseUrl", _wireMockUrl);
Environment.SetEnvironmentVariable("StockApi:BaseUrl", _wireMockUrl);
Enter fullscreen mode Exit fullscreen mode

We can have a single instance of wiremock that will mock both Carrier and Stocker services APIs.

Let's explore the API of Carrier-Service. It has a single "POST" endpoint that creates shipment in the carrier database.
The implementation is simplified.

public sealed record CreateCarrierShipmentRequest(
    string OrderId,
    Address Address,
    string Carrier,
    string ReceiverEmail,
    List<ShipmentItem> Items);

app.MapPost("/api/shipments",
    ([FromBody] CreateCarrierShipmentRequest request,
    ILogger<Program> logger) =>
{
    logger.LogInformation("Received create shipment request: {@Request}", request);
    return Results.Ok();
});
Enter fullscreen mode Exit fullscreen mode

And the Stock-Service has a "check" endpoint that verifies whether there are available products in the stock:

public record CheckStockRequest(List<Product> Products);

public record Product(string Name, int Quantity);

app.MapPost("/api/stocks/check",
    ([FromBody] CheckStockRequest request,
    ILogger<Program> logger) =>
{
    logger.LogInformation("Received check stock request: {@Request}", request);

    if (request.Products.All(x => x.Quantity <= 5))
    {
        return Results.Ok(new CheckStockResponse(true, "Products are available in stock"));
    }

    return Results.Ok(new CheckStockResponse(false, "Products are NOT available in stock"));
});
Enter fullscreen mode Exit fullscreen mode

Mocking APIs with WireMock

All mappings in WireMock are configured in JSON files and passed to WireMock docker image as a docker volume.

When running our integration tests locally on Windows from the IDE, we need to specify an absolute path to the mocking directory.
When running our integrations tests on Linux or inside a Docker container, we need to specify a relative path:

private readonly IContainer _wireMockContainer = new ContainerBuilder()
    .WithImage("wiremock/wiremock:3.7.0")
    .WithPortBinding(8080, true)
    .WithBindMount("mocks", "/home/wiremock")
    .Build();
Enter fullscreen mode Exit fullscreen mode

Next we need to create our mapping directory and files.
You need to specify "Copy If Newer" or "Copy Always" option for all the files in the "mocks" folder.

Screenshot_1

In the mappings folder you need to specify how you want WireMock to respond to your requests.

Here is how you can mock the Carrier-Service:

{
  "request": {
    "method": "POST",
    "url": "/api/shipments",
    "bodyPatterns": [
      {
        "matchesJsonPath": "$[?(@.orderId)]"
      },
      {
        "matchesJsonPath": "$[?(@.address)]"
      },
      {
        "matchesJsonPath": "$[?(@.carrier)]"
      },
      {
        "matchesJsonPath": "$[?(@.receiverEmail)]"
      },
      {
        "matchesJsonPath": "$[?(@.items)]"
      }
    ]
  },
  "response": {
    "status": 200,
    "bodyFileName": "CarrierShipmentResponse.json",
    "headers": {
      "Content-Type": "application/json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

You need to specify the following blocks:

  • request block: method, url, headers (optional), bodyPatterns (optional)
  • response: status (HTTP code), headers and body (text, json, or content from file)

In this example, we are checking whether a request has the desired schema.
It is essential to check whether our service sends a request with a correct payload.

If a payload is matched - a response is returned.
As we're working with JSON, you can either specify a response in the jsonBody or bodyFileName blocks.

In this example, I am returning a response from the separate JSON file that is located inside a "__files" folder.

The Response is straightforward:

{
  "message": "Shipment created successfully"
}
Enter fullscreen mode Exit fullscreen mode

Now let's explore how to mock a more complex use case with Stock-Service.
As I showed above, we simulate the behaviour of checking products availability in the stock.
In case the products count in the request is more than 5 — we return that products are NOT available in the stock.

if (request.Products.All(x => x.Quantity <= 5))
{
    return Results.Ok(new CheckStockResponse(true, "Products are available in stock"));
}

return Results.Ok(new CheckStockResponse(false, "Products are NOT available in stock"));
Enter fullscreen mode Exit fullscreen mode

In WireMock I've created 3 mocks for this request:

  1. stock-check-valid.json
{
  "request": {
    "method": "POST",
    "url": "/api/stocks/check",
    "bodyPatterns": [
      {
        "matchesJsonPath": "$.products[?(@.quantity > 0 && @.quantity <= 5)]"
      }
    ]
  },
  "response": {
    "status": 200,
    "bodyFileName": "StockCheckResponse.json",
    "headers": {
      "Content-Type": "application/json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. stock-check-no-products.json
{
  "request": {
    "method": "POST",
    "url": "/api/stocks/check",
    "bodyPatterns": [
      {
        "matchesJsonPath": "$.products[?(@.quantity > 5)]"
      }
    ]
  },
  "response": {
    "status": 200,
    "jsonBody": {
      "IsSuccess": false,
      "ErrorMessage": "Products are NOT available in stock"
    },
    "headers": {
      "Content-Type": "application/json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. stock-check-invalid.json
{
  "request": {
    "method": "POST",
    "url": "/api/stocks/check",
    "bodyPatterns": [
      {
        "matchesJsonPath": "$.products[?(@.quantity == 0)]"
      }
    ]
  },
  "response": {
    "status": 400,
    "jsonBody": {
      "error": "Quantity cannot be zero."
    },
    "headers": {
      "Content-Type": "application/json"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In these mock I use matchesJsonPath in bodyPatterns to write an expression that checks a given condition.
Based on the count of products in the request: I return products are found, not found in stock.
And I return "400 Bad Request" in case a request contains 0 number of products.

WireMock is very powerful in pattern matching for requests, you can find information about all available matches in their official docs.

Writing API Integration Tests

Now that we have set up WireMock and our integration tests, let's write our tests for creating a shipment use case:

[Fact]
public async Task CreateShipment_ShouldSucceed_WhenRequestIsValid()
{
    // Arrange
    var address = new Address("Amazing st. 5", "New York", "127675");

    List<ShipmentItem> items = [ new("Samsung Electronics", 1) ];

    var request = new CreateShipmentRequest("12345", address, "Modern Shipping", "test@mail.com", items);

    // Act
    var httpResponse = await factory.HttpClient.PostAsJsonAsync("/api/shipments", request);
    var shipmentResponse = (await httpResponse.Content.ReadFromJsonAsync<ShipmentResponse>(_jsonSerializerOptions))!;

    // Assert
    httpResponse.StatusCode.Should().Be(HttpStatusCode.OK);

    var expectedResponse = new ShipmentResponse(shipmentResponse.Number, "12345", address, "Modern Shipping", "test@mail.com", ShipmentStatus.Created, items);

     shipmentResponse
         .Should()
         .BeEquivalentTo(expectedResponse);
}
Enter fullscreen mode Exit fullscreen mode

It's very important to also test the error and validation response in our integration tests.

Let's create a test that checks whether "Products are NOT available in stock" error is correctly handled in our application and a shipment is not created:

[Fact]
public async Task CreateShipment_ShouldFail_WhenProductsAreNoAvailableInStock()
{
    // Arrange
    var address = new Address("Amazing st. 5", "New York", "127675");

    List<ShipmentItem> items = [ new("Samsung Electronics", 10) ];

    var request = new CreateShipmentRequest("12345", address, "Modern Shipping", "test@mail.com", items);

    // Act
    var httpResponse = await factory.HttpClient.PostAsJsonAsync("/api/shipments", request);
    var validationResult = await httpResponse.Content.ReadFromJsonAsync<ValidationProblemDetails>();

    // Assert
    httpResponse.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    validationResult?.Errors.Should().HaveCount(1);

    var error = validationResult?.Errors.FirstOrDefault();
    error?.Key.Should().Be("ProductsNotAvailableInStock");
    error?.Value.First().Should().Be("Received error from stock-service: 'Products are NOT available in stock'");
}
Enter fullscreen mode Exit fullscreen mode

And here is a test that verified that Bad Request is received when a request with 0 count of products is sent to the Stock-Service:

[Fact]
public async Task CreateShipment_ShouldFail_WhenZeroProductsSentInRequest()
{
    // Arrange
    var address = new Address("Amazing st. 5", "New York", "127675");

    List<ShipmentItem> items = [ new("Samsung Electronics", 0) ];

    var request = new CreateShipmentRequest("12345", address, "Modern Shipping", "test@mail.com", items);

    // Act
    var httpResponse = await factory.HttpClient.PostAsJsonAsync("/api/shipments", request);
    var validationResult = await httpResponse.Content.ReadFromJsonAsync<ValidationProblemDetails>();

    // Assert
    httpResponse.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    validationResult?.Errors.Should().HaveCount(1);

    var error = validationResult?.Errors.FirstOrDefault();
    error?.Key.Should().Be("ProductsNotAvailableInStock");
    error?.Value.First().Should().Be("Received error from stock-service: '{\"error\":\"Quantity cannot be zero.\"}'");
}
Enter fullscreen mode Exit fullscreen mode

Screenshot_2

Summary

WireMock is a powerful tool for mocking external APIs in .NET applications.
It allows you to create reliable and repeatable tests without depending on live services, ensuring that your application can handle various API responses and failures.
By using WireMock, you can focus on writing robust integration testing with external APIs, without a need to run these APIs.

On my website: antondevtips.com I share .NET and Architecture best practices.
Subscribe to become a better developer.
Download the source code for this blog post for free.

💖 💪 🙅 🚩
antonmartyniuk
Anton Martyniuk

Posted on October 1, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related