My team are pretty good at building integration tests, we use app.settings per environment, powershell and teamcity. Its fairly complicated but works really well. We dont always use WebApplicationFactory to bring up our API’s in memory as its not always sensible to do so.
I was inspired by a video from Nick Chapsas titled The cleanest way to use Docker for testing in .NET to investigate the nuget package Test Containers | Testcontainers 2.1.0.
Checkout Nick’s From Zero to Hero: Integration testing in ASP.NET Core course - amazing content brother! - Keep coding!
Problems faced with integration test
Testcontainers addresses a few problems faced with integration tests
Parallelize integration test execution
- Race conditions (collisions) where some tests fail because of orphaned data from another test. Example: ASelectAll
may expect 5 records in the database however a precedingInsert
may have changed this number to6
so theSelectAll
will fail its validation as6 > 5
Setup and teardown of the database
- Its overhead to have to worry about setting up infastructure outside of the code, another powershell or teamcity step which is just a possible point of failure.Create a database specific for test execution
- this will be in isolation, so for each test we will create a database using the Testcontainers mechanism.
Integration Tests
The samples assume you already have some integration tests setup, this is a simple CRUD API with integration tests over the controllers. My source code is based on Nicks video and can be downloaded at https://github.com/carlpaton/IntegrationTestsWithTestContainers.
My test classes are shown below. The problem with integration tests are they dont really customize the core project. Nick describes this as then calling the real database, the problem with this approach is we have test data remnants which we could delete after each test execution but that implies we need to have a database running at all times.
Other systems might be using this so we will run into problems with everyone running this locally and when we try do continuous integration.
The current integration tests are:
1 | CreateCustomerControllerTests |
Example Code
TestBase
I used TestBase to expose a HttpClient which is created using CustomerApiFactory -> WebApplicationFactory using the method CreateClient.
All tests then inherit TestBase
and use dependency injection to instanciate the CustomerApiFactory
using IClassFixture<CustomerApiFactory>
which is used to indicate a test has per-test-class fixture data.
1 | public class CreateCustomerControllerTests : TestBase |
CustomerApiFactory
To get started install the nuget package Testcontainers 2.1.0
I use WebApplicationFactory and the class
CustomerApiFactory
which would spin up an in-memory version of the CustomerApi.IAsyncLifetime is then used to manage the life time of PostgreSqlTestcontainer. This starts the container in the beginning of the tests and then stops them at the end of the tests using xUnits IAsyncLifetime interface which provides the methods
InitializeAsync
andDisposeAsync
which will run per test execution. As we have this in a collection it will run per class that has tests in it.
This is a great compromise between isolation of tests as its only limited to the collection.
- I then use override
ConfigureWebHost
to remove the DatabaseContext instance and replace it with our test instance, which uses theConnectionString
fromPostgreSqlTestcontainer
.
The value of this is the port is always unique, this is what the cool kids call an ephemeral container (a special type of container that runs temporarily in an existing Pod to accomplish user-initiated actions). So each test class gets a brand new and clean instance of a postgres database.
Note that we use ConfigureTestServices
instead of ConfigureServices
.
1 | public class CustomerApiFactory : WebApplicationFactory<Program>, IAsyncLifetime |
Running the tests now will spin up a container for postgres just for the test collection. This means we can seed any test data we may want during startup and we dont need to worry about clearning it out as the containers are ephemeral.
The tests can now run and their container will automagically be spun up <3. Notice each have their own port and cannot be polluted with another tests data. Additionally the containers are deleted by the framework. The is the most hard out nerd thing I’ve seen in a while :D
Under the hood the container orchestration is done by testcontainers / moby-ryuk
- Currently there is support for other databases listed below. For ones that dont exist its possible to use 3rd party packages ontop using the Fluent API described below.
See https://github.com/testcontainers/testcontainers-dotnet#pre-configured-containers for the latest list.
1 | CouchbaseTestconnectionConfiguration |
Test Container Fluent API
The test container framework has a fluent API that allows us to load up any docker image that is not natively supported, this is an example with drawbacks such as static port.
All of the commands are listed here - https://github.com/testcontainers/testcontainers-dotnet#supported-commands
1 | // private member for test containers using fluent api to construct the builder |