Creating databases on demand
One of the biggest, if not the biggest, problems we need to deal with when we are running integration tests is having a database in place for the tests to run against. This datastore needs to be controlled for those tests to run and satisfy specific criteria such as containing specific data for the tests to run.
Managing and maintaining a database that runs at all times just for the integration tests to run against, is not a scalable option. Wouldn't it be better if we could create an isolated database only when the tests run and then shut it down once the tests pass or fail?
Well what a coincidence! That's exactly what we'll learn how to do.
Introducing Docker
Docker is a containerization technology that allows us to run services in a controlled and self-contained fashion. We can run anything with it, from web services, to reverse proxies, to databases. In fact this is how I've been running the Postgres database that I've been using until now. Docker containers are lightweight so we can create and destroy them on demand very easily. Such a concept lends itself really nicely for our integration testing usecase.
Running Docker Containers with C#
The first thing we need to do is to find a way to control Docker with C#. There are a couple of main ways and over the course of this workshop we will see both. We will start however with the simplest one for our usecase.
We will use a Nuget package called Testcontainers
.
dotnet add package Testcontainers
We can now use the IContainer
class in our code to define the container that we need.
Since the CustomerApiFactory
is responsible for managing the lifetime of our tests, we will add the container creation and shutdown code there.
Defining the Testcontainer
As a reminder, this is how we have defined our database in the docker-compose.yml file that we used to run it.
image: postgres:latest
restart: always
environment:
- POSTGRES_USER=workshop
- POSTGRES_PASSWORD=changeme
- POSTGRES_DB=mydb
ports:
- '5432:5432'
To define the same thing using Testcontainers we need to created a IContainer
object and use the ContainerBuilder
's fluent extensions to define those items.
For example to define the image: postgres:latest
we need to call the WithImage
method.
private readonly IContainer _dbContainer =
new ContainerBuilder()
.WithImage("postgres:latest")
.WithEnvironment("POSTGRES_USER", "workshop")
.WithEnvironment("POSTGRES_PASSWORD", "changeme")
.WithEnvironment("POSTGRES_DB", "mydb")
.WithPortBinding(5555, 5432)
.WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(5432))
.Build();
The WithWaitStrategy
ensures that the port is ready before the tests continue. This makes sure the tests won't run against a database container that hasn't started yet.
Now to start the container we need to call the StartAsync
method of the object in the InitializeAsync
method and the StopAsync
method in the DisposeAsync
one.
public async Task InitializeAsync()
{
await _dbContainer.StartAsync();
HttpClient = CreateClient();
_dbConnection = new NpgsqlConnection("Server=localhost;Port=5555;Database=mydb;User ID=workshop;Password=changeme;");
await InitializeRespawner();
}
public new async Task DisposeAsync() => await _dbContainer.StopAsync();
But wait a second. How can we configure our API to use the new connection to this ephemeral database?
Re-wiring the database connection
Re-configuring the web application is extremely easy due to the ConfigureWebHost
that we have in the custom WebApplicationFactory.
We can simply call the builder.ConfigureTestServices
method and manipulate the service container in any way that we want for our testing.
In our case we want to remove the existing IDbConnectionFactory
and add one that points to our Docker database.
We can do that with a couple of line of code:
builder.ConfigureTestServices(services =>
{
services.RemoveAll(typeof(IDbConnectionFactory));
services.AddSingleton<IDbConnectionFactory>(_ => new NpgsqlConnectionFactory("Server=localhost;Port=5555;Database=mydb;User ID=workshop;Password=changeme;"));
});
Now if we run our tests you can see that all of them pass, without having a database running beforehand and the database created for the tests is deleted immediately after the tests run.
And that's it! We now create a database on demand just for out tests and we tear it down after we are done.
Using Database-specific Test containers
Testcontainers can help us run containers very easily as we've already seen but they also have specific code for very common container images such as database. These pre-defined containers can streamline our code a lot.
For example in our previous example, we hardcoded the port 5555
both in the container definition and the connection string.
However, we don't know for a fact that this port will always be free on our machine. We could write code that deals with that but we don't need to.
The first thing we need to do is add the Nuget package of the appropriate database or "Module".
In our case it is Postgres so the Nuget package we need is Testcontainers.PostgreSql
.
We can now change our IContainer
to be of type TestcontainerDatabase
and use a builder of type PostgreSqlBuilder()
.
This allows us to use the WithXXX
extension methods to define our database settings.
Now all the code we need to define the Postgres database looks like this:
private readonly PostgreSqlContainer _dbContainer =
new PostgreSqlBuilder()
.WithDatabase("mydb")
.WithUsername("workshop")
.WithPassword("changeme")
.Build();
And what's even better, we no longer need to hardcode and connection strings or ports. Testcontainers will manage all that for us.
builder.ConfigureTestServices(services =>
{
services.RemoveAll(typeof(IDbConnectionFactory));
services.AddSingleton<IDbConnectionFactory>(_ => new NpgsqlConnectionFactory(_dbContainer.GetConnectionString()));
});
Now simply run the tests and watch them pass without you needing to have a database in place.