Technology Enthusiast, Software Engineer, Architect & Craftsman, DevOps Human & All Round Disney Lover

Simplifying Integration Testing with ASP.NET Core and Testcontainers

Simplifying Integration Testing with ASP.NET Core and Testcontainers

Dan Horrocks-Burgess
Dan Horrocks-Burgess

Whilst working on a project for a new client, I wanted to include integration tests that tested the full stack of a newly developed ASP.NET API, using Test-Driven Development (TDD) to iterate and improve as I went. ASP.NET Core provides convenient test host NuGet packages to run an “In-Memory” version of the newly developed API. However, I also wanted to test the end-to-end stack of the system including the database.

In the past, I would have either mocked at the lowest level in the tech stack or used the In-Memory Entity Framework Core provider when working with Entity Framework. However, upon further reading, I discovered that Microsoft has openly documented that they are no longer adding any new features to the in-memory provider and even discourage its use for testing.

This database provider allows Entity Framework Core to be used with an in-memory database. While some users use the in-memory database for testing, this is discouraged. https://learn.microsoft.com/en-us/ef/core/providers/in-memory/?tabs=dotnet-core-cli

The Solution

Testcontainers is an “open source framework for providing throwaway, lightweight instances of databases, message brokers, web browsers, or just about anything that can run in a Docker container.” Simply put, an easy and simple way for me to spin up a real SQL Server Database in a Docker container, use it for the lifetime of my test run, and then throw it away. No mess, no fuss, no mocks!

It’s important to note that this solution does come with some additional requirements, such as needing a functional Docker runtime on the host system, and the ability to run Docker containers on your build hosts (for example, running tests on GitHub Actions hosted runners whilst running a continuous integration build). I’m happy to say that running these tests on GitHub’s runners works without any changes!

Since the project I was working on is fully containerised, runs locally on K8s, and the developer experience is as close to Production as possible (a blog post for another time), using Testcontainers was a great solution.

The Code

Example code can be found on an example GitHub repository I’ve created: https://github.com/DanHorrocksBurgess/example-dotnet-api-integration-testing

To get this running, after installing the `Testcontainers.MsSql” NuGet package I updated the test fixture, which runs once per test run (with a supported collection) to do the following:

  • Create and start a new MSSQL container
  • Run the EF Core migrations (Enabling the database to be ready to run tests against)
  • Remove the DBContext service in the API startup, re-adding it again with the newly created MSSQL container connection string.
public class SharedApiTestFixture : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly MsSqlContainer _msSqlContainer = new MsSqlBuilder().Build();

    // Configure Application Host - Configuration
    protected override IHost CreateHost(IHostBuilder builder)
    {
        return base.CreateHost(builder);
    }

    // Configure Web Host - Services etc...
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions<ApiContext>));
            if (descriptor != null) services.Remove(descriptor);

            services.AddDbContext<ApiContext>(options => { options.UseSqlServer(_msSqlContainer.GetConnectionString()); });
        });
    }


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

        using var context = CreateContext();
        await context.Database.MigrateAsync();
    }

    public ApiContext CreateContext()
        => new(
            new DbContextOptionsBuilder<ApiContext>()
                .UseSqlServer(_msSqlContainer.GetConnectionString())
                .Options);

    public new async Task DisposeAsync()
    {
        await _msSqlContainer.DisposeAsync();
        await base.DisposeAsync();
    }
}

What next?

This is a super simple example, with limitations. For example, one test’s data can easily affect another test, I could clear down the data manually between each test or use something like Respawn to manage the state and reset the DB after each test / test collection / test fixture.