When creating integration tests it can often be that external resources such as databases are:
- Based upon a SQLite database (via
Microsoft.EntityFrameworkCore.Sqlite
) - Based upon an in-memory database (via
Microsoft.EntityFrameworkCore.InMemory
)
In both of these cases you are not testing against the actual database you would be using in your production environment. If you run against PostgreSQL in production, you should really be running your integration tests against the same database platform.
The challenge of this as close-to-production style of integration testing is having a PostgreSQL database consistently available without having to perform a lot of manual intervention. This is before we even begin to consider how that database would be seeded with a schema and data to support whatever tests we intend to run.
The Test Containers open source project solves this problem by providing throwaway instances of databases, message brokers and a whole host of other services via Docker. Test Containers allow us to:
- Mirror as close to production infrastructure in our integration tests
- Programatically spin up and configure containers before our integration tests commence
- Automatically destroy containers upon completion of integration tests
You can view all the supported module/container types here and you will find great supporting documentation for use with the .NET Framework here.
For the purposes of this example we want to use a PostgreSQL container loaded with movie data to support our /api/movies
endpoint in our API
project.
Note: to use this project you will need to ensure you have Docker installed and running on your workstation.
There is docker-compose.yml
file in the root of the cloned repository. If you do:
docker compose up -d
This will start the database. If you do:
docker compose down --volumes
This will stop the PostgreSQL container and remove its associated volumes. If you wish to keep re-using the container once it is created just omit the --volumes
part of the above command and your data will remain between up/down operations.
In the root of the cloned repository you will find the etc/docker-entrypoint-initdb.d
directory. This directory is mounted into the container when it is created. This directory contains a file called 01-create-movies-db.sql
which will be executed within the container the first time it starts. This script creates:
- A new database called
movies
- A new user called
moviesuser
with a password and appropriate permissions - Connects to the
movies
database and creates the tables and data required by the application
The appsettings.json
file in the API
project has a connection string called MoviesDb
that can connect to this container whilst running and debugging.
In this project we depend upon the following Test Containers nuget package:
Testcontainers.PostgreSql
This nuget package provides the ability to create short-lived PostgreSQL docker image configured with:
- A specific PostreSQL image/version
- A named database with a username and password
- Port Bindings
- Volume Bindings
The tests in this project make use of xUnit and Fluent Assertions to orchestrate our tests.
If we look at the test
TestingContainersExample.Tests.Integration.API.Controllers.MoviesControllerTests
we want to have our database available for the lifetime of the test suite. We can use a WebApplicationFactory
alongside a PostgreSqlContainer
test to achieve this.
The test class implements/extends IClassFixture<IntegrationTestWebApplicationFactory>
. xUnit class fixtures are a shared context that exists for all tests in the class. In our case this is:
- A
WebApplicationFactory
hosting our API. - An instance of a
PostgreSqlContainer
we can connect to from ourMoviesDbContext
in the service collection of the web application.
In the constructor of IntegrationTestWebApplicationFactory
we use a PostgreSqlBuilder
to define what our PostgreSQL image should contain. You will see it:
- Uses the very latest PostgreSQL image
postgres:latest
- Names the database
movies
- Creates a user called
moviesuser
with an associated password - Mounts the
scripts/docker-entrypint-initdb.d
directory of the project into/docker-entrypoint-initdb.d
within the container. This directory contains the script01-create-movies-db-data.sql
which will create our schema objects and data when the container starts. - Assigns a random external port from the container which can be used to connect to the database
This class also implements IAsyncLifetime
and when the instance is given to the test class the InitializeAsync()
method is called. This will trigger the PostgreSQL Test Containers instance to start. When the test class finishes with the fixture the DisposeAsync()
method will be called. This stops the PostgreSQL test container and destroys/removes it from your docker instance.
This same class extends WebApplicationFactory<Program>
. When the overriden ConfigureWebHost
method is invoked it:
- Finds and removes the
DbContextOptions<MoviesDbContext>
which was already loaded into the service collection withservices.AddDbContext<MoviesDbContext>
inprogram.cs
- Calls
services.AddDbContext<MoviesDbContext>
to re-create theMoviesDbContext
using the connection string obtained from the test container.
As we define the container with .WithPortBinding(5432, assignRandomHostPort: true)
this means this PostgresSQL instance will not collide with any other instances of Postgres you may have running in Docker (especially if they use the default port 5432
)
This test provides a slightly different approach to using the PostgreSQL test container. Within this test we are only testing the TestingContainersExample.Common.Services.MovieService
class so we don't need the whole application to be spun up - we only require a MoviesDbContext
in order to exercise the service.
If you look at TestingContainersExample.Tests.Integration.Fixtures.MoviesDbContextFixture
you will see that this class only provides a mechanism to obtain a MoviesDbContext
. The test goes on to create an instance of the MoviesService
to which it can provide the MoviesDbContext
created by the fixture class.
Using test containers provides a very nice mechanism to allow you to test your code in a way that more closely matches your production infrastructure. In this instance we are only making use of a database test container but if you require other services like Kafka, RabbitMq, Redis etc. they are all supported. Thanks for looking.