DEV Community

Cover image for Testing .NET APIs with Testcontainers and Mockoon
Victor Abud
Victor Abud

Posted on

Testing .NET APIs with Testcontainers and Mockoon

Building reliable services goes beyond writing clean code. It requires a strong testing strategy that validates how your application behaves in real‑world scenarios. While unit tests ensure that business logic works in isolation, integration tests validate the different components of a service, requiring using external dependencies like databases, cloud providers or third‑party APIs.

This is where tools like Testcontainers and Mockoon become invaluable. Testcontainers allows you to spin up real, disposable infrastructure components inside lightweight containers, giving your integration tests an authentic environment. On the other hand, Mockoon provides a simple yet powerful way to simulate external APIs, letting you control responses, test error conditions, and validate how your application handles edge cases.

In this post we’ll adapt a .NET project to use mocks created using Mockoon and start containers to consume them in the testing context of the API.

The .NET API context

For this example, we’ll assume that we have a simple API that exposes an endpoint that sends SMS using the Twilio service. The structure of the project is simplified: we have a minimal API with an endpoint, and the following configuration in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Register configuration
builder.Services.Configure<TwilioOptions>(
    builder.Configuration.GetSection("Twilio"));

builder.Services.AddScoped<ITwilioService, TwilioService>();

var app = builder.Build();

app.MapPost("/send", async (
    [FromServices] ITwilioService twilioService,
    [FromBody] MessageRequest request) =>
{
    var result = await twilioService.SendMessageAsync(request);
    return result ? Results.Ok() : Results.BadRequest();
});

app.Run();
Enter fullscreen mode Exit fullscreen mode

In order to manage a different base URL for the Twilio service when running the tests or when debugging/deploying de API, the Options Pattern can be used, allowing to change the default values per environment of the configuration settings assigned in the appsetting.json file. The example implementation service might look like:

public class TwilioService : ITwilioService
{
    private readonly HttpClient _httpClient;
    private readonly IOptions<TwilioConfigurations> _config;

    public TwilioService(IOptions<TwilioConfigurations> config,
        HttpClient httpClient)
    {
        _config = config;
        _httpClient = httpClient;
        _httpClient.DefaultRequestHeaders.Add("Authorization", $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_config.Value.AccountSid}:{_config.Value.AuthToken}"))}");
    }
    public async Task SendSms(string message, string to)
    {
        var url = $"{_config.Value.BaseUrl}/Accounts/{_config.Value.AccountSid}/Messages.json";

        var request = new HttpRequestMessage(HttpMethod.Post, url);

        var collection = new List<KeyValuePair<string, string>>
        {
            new("To", to),
            new("From", _config.Value.From),
            new("Body", message)
        };

        request.Content = new FormUrlEncodedContent(collection);

        var response = await _httpClient.SendAsync(request);

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception("Failed to send SMS");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

If the SendMessage endpoint is consumed, this will use the appsettings configurations, so the SMS will be sent using the real Twilio service URL configured. Once the service is ready, it’s time to create the mock definition.


Mocking REST APIs with Mockoon

Mockoon is a powerful tool that allows to create different mocks of APIs and Websockets. In this example, will be mocked the Twilio Send Message endpoint with 2 cases: an ✅ OK response and a ❌ BadRequest response. The quantity of responses and rules will depend of how many scenarios are needed to test our code. In the Mockoon UI the scenarios might look like this:

Mockoon example


Creating and building the mock image

Once the Mockoon JSON definition of the service to test is ready, it’s time to build the Docker image that will start the Mockoon server and expose the mocked API in the container. In this case, can be used a simple Node image, install de Mockoon CLI dependencies and copy the Mockoon JSON definition. The Dockerfile might look like this:

FROM node:18-alpine

RUN npm install -g @mockoon/cli@9.3.0

WORKDIR /app

COPY TwilioMocks.json /app/TwilioMocks.json

EXPOSE 3000

ENTRYPOINT ["mockoon-cli", "start", "--data", "/app/TwilioMocks.json", "--port", "3000"]
Enter fullscreen mode Exit fullscreen mode

Integrating Testing and Testcontainers

Testcontainers is an amazing open source library that allow us to run Docker containers, handling the creation and deletion of the containers, giving the possibility of creating databases or any kind of service that can run in containers.

Now that the mock of the service and the built image with the mock definition are ready, the next step is creating the test layer the project and add the Testcontainers logic to start the containers. Let’s start with our WebApplicationFactory class:

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    private readonly IContainer _mockoonContainer;

    public CustomWebApplicationFactory()
    {
        _mockoonContainer = new ContainerBuilder()
            .WithImage("mockoon-twilio:latest")
            .WithPortBinding(3000, true)
            .Build();
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Override Twilio URL to point to Mockoon container
            services.PostConfigure<TwilioOptions>(options =>
            {
                options.BaseUrl = _mockoonContainer.GetMappedPublicPort(3000).ToString();
            });
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

In this class is implemented the WebApplicationFactory abstraction with our Program class as the entrypoint. This will allow to add new dependency injections or change the values of the existing ones. In this case, we’ll use it to start the container using TestsContainers, then changing the Twilio service URL to the container’s host URL. In order to use this implementation, we create the SetUpFixture class:

[SetUpFixture]
public class TestSetup
{
    private CustomWebApplicationFactory _factory;

    [OneTimeSetUp]
    public async Task OneTimeSetup()
    {
        _factory = new CustomWebApplicationFactory();
        await _factory.Services.GetRequiredService<IHostedService>().StartAsync(CancellationToken.None);
    }

    [OneTimeTearDown]
    public async Task OneTimeTearDown()
    {
        await _factory.DisposeAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

This class will be executed before running the tests, this is specified by the SetUpFixture attribute of NUnit. Using the WebApplicationFactory is possible to create a HttpClient instance that will be used for consuming the minimal API in testing. For this post there are just 2 scenarios using the HttpClient:

[TestFixture]
public class MessageTests
{
    private HttpClient client = new();

    [SetUp]
    public void Setup()
    {
        client = TestStartup.HttpClient;
    }

    [Test]
    public async Task SendMessage_WhenMessageIsSent_ShouldReturnSuccess()
    {
        var request = new MessageRequestModel
        {
            Message = "This thest should pass",
            To = "+19514004113"
        };

        var response = await client.PostAsJsonAsync("/api/v1/messages", request);
        TestContext.Out.WriteLine(await response.Content.ReadAsStringAsync());

        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
    }

    [Test]
    public async Task SendMessage_WhenMessageIsNotSent_ShouldReturnError()
    {
        var request = new MessageRequestModel
        {
            Message = "This test should pass when an error occurs",
            To = "+1838383838" // The phone number that the mock will respond with a 400 error
        };

        var response = await client.PostAsJsonAsync("/api/v1/messages", request);

        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.InternalServerError));
    }
}
Enter fullscreen mode Exit fullscreen mode

The tests results look like:

Tests results


Conclusions

In summary, using Testcontainers and Mockoon to test services offers a balance between realism and speed. Testcontainers ensures that your integration tests hit real, containerized dependencies, making your tests highly reliable and reflective of production environments.

At the same time, Mockoon allows to simulate REST endpoints with precision and flexibility — whether you need static responses, dynamic templating, proxying, or advanced rules.


The example code of this post in this repository.

The original post was published on Medium

Links & References

Top comments (0)