Introduction
Integration tests are essential to ensure that the different components of our system work together as expected and continue to work after changes.
In this post, I’ll explain how to spin up disposable database containers to use in integration tests.
Integration tests and managed resources
As explained in this article, in integration tests, we should mock unmanaged dependencies (dependencies that are external to our system and not controlled by us, like APIs) but test against real managed dependencies (dependencies that are controlled by our system, like databases). This improves the reliability of the integration tests because the communication with these dependencies are a complex part of the system and can break with a package update, a database update or even a simple change in a SQL command.
What is Testcontainers?
Testcontainers is a library that provides lightweight, throwaway instances of databases, selenium web browsers, or anything that can run in a container. These instances can be especially useful for testing applications against real dependencies, like databases, that can be created and disposed of after the tests.
Why not run the containers manually?
One key benefit of using Testcontainers instead of running the containers manually is that we can make use of libraries such as AutoFixture to generate data to seed the database instead of running scripts to insert the data. This also helps in avoiding collision between data used in different tests because the data is random.
Also, there are other advantages in usings Testcontainers:
- To run the tests locally, you don’t need any extra steps, like running a docker-compose command;
- You don’t have to wait or implement a waiting strategy to check if the containers are running before accessing them. Testcontainers already implements this logic;
- Testcontainers have properties to access the port in which the container is running, so you don’t need to specify a fixed port, avoiding port conflict when running in the CI/CD pipeline or other machines;
- Testcontainers stops and deletes the containers automatically after running, free resources on the machine.
Running a disposable container with Testcontainers
To use Testcontainers, you will need to have a container runtime (Docker, Podman, Rancher, etc) installed on your machine.
Then, you need to add the Testcontainers NuGet package to your test project:
dotnet add package Testcontainers --version 3.1.0
dotnet add package Testcontainers.MySql --version 3.1.0
⚠️ Since the writing of this post, Testcontainers released version 3.1.0 and made breaking changes. I’m updating this post to reflect these changes. To see the original code with Testcontainers 2.3.*, look in this branch of the repository.
To run a container, we first need to use the ContainerBuilder
class to build a DockerContainer
or a derived class, for instance, MySqlBuilder
:
|
|
Then, we start the container and use the GetConnectionString
method where needed:
|
|
❗
DockerContainer
implementsIAsyncDisposable
and needs to be disposed of after use. We can use theawait using
syntax or call theDisposeAsync
method.
Testcontainers have classes for many different databases (called modules), for example:
- Elasticsearch;
- MariaDB;
- Microsoft SQL Server;
- MySQL;
- Redis.
The full list can be seen here.
But we can also create containers from any image, as in the example below, where it creates a Memcached instance:
|
|
💡 In this post, I explain how to use Testcontainers with Localstack to create integration tests for systems that use AWS services.
More details in the official documentation.
Creating integration tests with Testcontainers
In this example, I’m using the xUnit and the WebApplicationFactory<T>
class from ASP.NET Core.
If you don’t know how to use the WebApplicationFactory<T>
class, I explained in this post.
In this example, I have a controller with a GET
method that returns a ToDo item and a POST
method that add a ToDo item:
|
|
⚠️ The business logic is in the controller just for the sake of simplicity. In a real-world application, the logic should be in another layer.
Testing the GET method
The test does the following actions:
- Create and start a MySql container;
- Create a Entity Framework DbContext using the ConnectionString from the MySql in the container;
- Create the database tables using Entity Framework; (Can also be done passing a script to the
ExecScriptAsync
method from themySqlContainer
object); - Create a random object with AutoFixture and add it to the database table;
- Override our application configuration with the connection string from the container;
- Create an
HttpClient
pointing to our application; - Make a request to the GET endpoint passing the Id of the random object we added to the database;
- Validate that the Status Code is
200
and the object returned is the same we added to the database.
|
|
❗ Be aware that I pass the container tag version in the
WithImage
method even when using the typedMySqlBuilder
class. This is very important because when we don’t pass the tag, the container runtime will use thelatest
tag and a database update may break the application and the tests.
Testing the POST method
First, let’s migrate the MySqlContainer
and the DbContext
creation to a Fixture and a Collection so they can be shared between all tests. This is recommended because unless we do this, all tests will spin up and dispose of the container, making our tests slower than needed.
|
|
The test does the following actions:
- MySql container and DbContext are injected by the xUnit fixture;
- Create a random object with AutoFixture;
- Override our application configuration with the connection string from the container;
- Create an
HttpClient
pointing to our application; - Make POST request to the endpoint passing the random object previously created;
- Validate that the Status Code is
200
and that theLocation
header has the correct URL for the GET endpoint of the created object; - Query the database and validate that the object created is equal to the randomly created object.
|
|