Mike Jeffs

Writing .NET Integration tests with Testcontainers

This tutorial will demonstrate how to setup a Testcontainer running PostgreSQL for running integration tests on a ASP.NET Core Web API. For database access, both Entity Framework and Dapper are covered in this tutorial.

Prerequisites

Setup

Create a new xUnit project, and within it install any dependencies you require (e.g. fluentAssertions, Bogus for fake data, etc.). You also need to add a reference to the Web API project in the test projects csproj file.

Once the project is created, Install the Testcontainers.PostgreSql package to be able to run a postgres database in a container. You’ll also need to install the Microsoft.AspNetCore.Mvc.Testing package from nuget. This package includes the WebApplicationFactory for creating a ‘mocked’ version of our API, so that we don’t need to manually run our API each time we want to run our tests.

You may also need to create an https certificate for your API in you haven’t done so already. To do this, open a terminal, navigate to your API project, and simply run the following command: dotnet dev-certs https -ep cert.pfx -p Testy123!

WebApplicationFactory

Once the Microsoft.AspNetCore.Mvc.Testing package is installed, we can create a class that inherits the WebApplicationFactory. This is where we will setup our Testcontainer, as well as our http client to make the calls to our API. The class will look like this:

public class ApiFactory : WebApplicationFactory<IApiMarker>, IAsyncLifetime
{
	public Task InitializeAsync() => Task.CompletedTask;
	public Task DisposeAsync() => Task.CompletedTask;
}

Here, the WebApplicationFactory is giving us a bootstrapped API to run in memory for our tests. We inherit from it to override certain behaviours, such as registering our database container and other services required for our tests.

Notice the generic type in the WebApplicationFactory declaration ‘IApiMarker’. This lives in the API project, and as the name suggests, it’s just a empty interface marker to your API, which enables the WebApplicationFactory to easily find the API it is bootstrapping. You can use the ‘Program.cs’ file in the API, but it is generally considered a better practice to use a marker interface.

The factory should implement the IAsyncLifetime interface, which implements two methods, InitializeAsync and DisposeAsync. These async methods are where we are going to initialise and teardown our testcontainer(s) and other resources.

Creating a Testcontainer

Within the ApiFactory class created in the previous section, create a property for the Postgres container for our database like so:

private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
    .WithDatabase("<your-db-name>")
    .WithUsername("postgres")
    .WithPassword("postgres")
    .WithPortBinding(5432, 5432)
    .WithWaitStrategy(Wait.ForUnixContainer()
        .UntilPortIsAvailable(5432))
    .Build();

Since this container will be running for the lifetime of our tests only, and then spun down when no longer needed, and as they’re running locally on our machine, the username and password can be anything, but make sure to set the database name. The .WithPortBinding is not strictly necessary unless you find that your container does not start on the expected port by default. With it, you can explicitly set the port binding, where the first parameter is the host port, and the second being the internal container port that the service is running on. The WithWaitStrategy is just there as a precaution in case a local database is already running on that port, you can leave it out if you wish, but essentially it will wait until the port is available before starting.

Adding an HTTP client

We also need to setup an HTTP client in order to run our integration tests against our API. I like to set it up within the ApiFactory class, but you can create them in the test file(s) if you prefer. The WebApplicationFactory provides a method called CreateClient() that returns an instance of HttpClient. We just need to define a public property in the factory (with a private setter so it can only be assigned by the factory). public HttpClient HttpClient { get; private set; } The assignment of this property must be done within the InitializeAsync method, but more on this in the next section.

Starting and Stopping our factory resources

For any Testcontainers declared in our factory, to start them up they must be called from the InitializeAsync method. In this example, we’re using a PostgreSQL container, which includes a StartAsync method. For our HttpClient that we defined earlier, this must be created within the InitializeAsync method, but AFTER the database container is started. The ordering here matters, because if you create the http client before starting the db container, your integration tests will fail as they will make the API call before the database is fully up and running, thus causing a database connection failure.

public async Task InitializeAsync()
{
    await _dbContainer.StartAsync();
    HttpClient = CreateClient(); // This must come AFTER the db is started.
}

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

Call StopAsync on the Testcontainer within the DisposeAsync method to ensure the container is torn down when all integration tests have completed. (the ’new’ keyword is required in the DisposeAsync method to prevent hiding the ValueTask override.)

Setting up the ORM

EF Core - configuring the Testcontainer to work with your DbContext

If your project is using EntityFramework core, follow the steps below:

The Testcontainer needs to be configured with EF Core in order for it to be used for your tests. The WebApplicationFactory allows us to override the ConfigureTestServices method, in here we can override registrations to our applications service collection for dependency injection. In this case we need to remove all registrations of our existing DbContext and register a new one that uses the connection string of our Testcontainer, rather than the APIs one:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    builder.ConfigureTestServices(async services =>
    {
        services.RemoveAll(typeof(AppDbContext));
                services.AddDbContext<AppDbContext>(opt =>
        {
            opt.UseNpgsql(_dbContainer.GetConnectionString(), npgsqlOptions =>
            {
            });
        });
    });
}

In my case, I’m using a defined context called ‘AppDbContext’, with a single DbSet of users. Replace this with your own defined context as required.

public class AppDbContext : DbContext {
	public DbSet<User> Users { get; set; }
}

In the example above, we are removing all registrations of the AppDbcontext that will have occurred in our application code, and registered a new one using the connection string of our created Testcontainer.

Dapper

If you’re using dapper as your ORM, follow the steps below to get your application to use the Testcontainer:

Create an interface called IDbConnectionFactory, with a method that returns an IDbConnection.

public interface IDbConnectionFactory
{
  Task<IDbConnection> CreateConnectionAsync(CancellationToken cancellationToken = default);
}

And then an implementation that uses the db of your choice, in this case it’s PostgreSQL:

public sealed class PostgresDbConnectionFactory(string connectionString) : IDbConnectionFactory
{
    public async Task<IDbConnection> CreateConnectionAsync(CancellationToken cancellationToken = default)
    {        var connection = new NpgsqlConnection(connectionString);
        await connection.OpenAsync(cancellationToken);
        return connection;
    }}

Now we just have to remove the APIs DI registration for the IDbConnectionFactory, and then add a new one that points to the test container. The WebApplicationFactory allows us to override the ConfigureTestServices method, in here we can override registrations to our applications service collection for dependency injection.

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    builder.ConfigureLogging(logging =>
    {
        logging.ClearProviders();
    });
    builder.ConfigureTestServices(services =>
    {
        services.RemoveAll(typeof(IDbConnectionFactory));
        services.AddSingleton<IDbConnectionFactory>(_ =>
            new PostgresDbConnectionFactory(_dbContainer.GetConnectionString()));
    });}

At this point, our ApiFactory should be all setup and ready to go. Now we move on to writing some integration tests!

Creating an integration test

The following example is testing an API endpoint that takes in user input to create a new user and save it to the database. The Bogus package is used to generate some fake but ‘realistic’ looking data that passes validation. The HttpClient is then used to call the POST create user endpoint in our API, passing in the request data, and waiting for the expected response. (This test assumes that the inputs provided are valid).

This test class implements the IClassFixture interface. This interface comes from xUnit, and it allows for the API factory instance created to be shared among any and all tests that are defined in this class (instead of spinning up a new instance of the API in memory for each test). The api factory can then be injected into the constructor of the test class, and assigned to a variable for use.

public class CreateUserEndpointTests : IClassFixture<ApiFactory>
{
    private readonly ApiFactory _apiFactory;
    private readonly Faker<CreateUserRequest> _createUserRequestGenerator;
    private readonly List<Guid> _createdUserIds = [];

    public CreateUserEndpointTests(ApiFactory apiFactory)
    {
	    _apiFactory = apiFactory;
        _createUserRequestGenerator = new Faker<CreateUserRequest>()
            .RuleFor(x => x.Email, f => f.Person.Email)
            .RuleFor(x => x.Password, f => "AbcdefghijklMNOP12345")
            .RuleFor(x => x.Username, f => f.Person.UserName)
            .RuleFor(x => x.DateOfBirth,
	            f => DateOnly.FromDateTime(f.Person.DateOfBirth));
    }

    [Fact]
    public async Task GIVEN_Create_WHEN_DataIsValid_THEN_CreateUser()
    {

	    var request = _createUserRequestGenerator.Generate();

		var response = await _apiFactory.HttpClient.PostAsync(
           "/api/v1/users",
           new StringContent(
           JsonSerializer.Serialize(request),
           Encoding.UTF8,
           "application/json"));

       var stringResponse = await response.Content.ReadAsStringAsync();

	   response.StatusCode.Should().Be(HttpStatusCode.Created);
       stringResponse = stringResponse.Trim('"');
       var createdUserId = Guid.Parse(stringResponse);
       stringResponse.Should().NotBeNullOrEmpty();
    }
}

By following all the steps in this tutorial, you should now have a working integration test for your Postgresql database running in a Testcontainer!