Lament
While writing about running tests with databases, I realized that I’ve been doing a lot of posts on testing recently. I guess that’s getting old for you. Some people buy a shiny red sports car, I try to write better tests. Not just because we have code coverage thresholds but because I think good tests can help you write better code, good tests can actually speed up your coding because you can easily test what you’ve just written, and bad tests just give you a false sense of security.
How often have you heard somebody say we have X% coverage (X > 80)? Does that give you the certainty that the code is good or that they have unnecessary tests? I believe in tests that make sense, and not the tests, as I’ve seen, that basically test if MediatR or Entity Framework work as expected.
Problems with testing with databases
Unit tests can get only get you so far and at one point you’re going to need to do something more substantial, which will usually include a database.
There are many solutions out there on how to actually incorporate a database layer into your tests. Some of the more popular ones are:
- Fake (mock) it till you make it
- local db (be it an instance or the lightweight MS provided one)
- H2 if you’re in Javaverse
- Docker
And each has its own set of problems. Mocking it is tedious and you’re more or less testing if your mocking framework works. The local database is not the same as the database you’re going to be deploying on. That’s why, if it’s a hosted database, you have to do cleanup and maintenance work.
H2 is beautiful to work with, but it hasn’t been ported to .NET world last I checked. Docker is annoying if you have to manually maintain the containers and connection strings. And when you’re using CI pipeline, it’s very annoying to set everything up, as anyone who tried will attest.
Code based Docker
It would be beautiful if our tests, let’s call them end-to-end, would be self-contained, idempotent and easy to write.
That’s how the idea that developed into this code started. After some trial and error, I’ve managed to achieve it. Basically, we start a docker container from code, create the database there, run the tests and shut it down afterwards. And since you can modify the image, you can get very very close to your deployment environment to feel confident about your code.
The repository is here. Feel free to do what you want with it, and the explanation of how the code works is below.
Testing with databases – setup
You’re going to need to install Docker. Windows instructions are here and I won’t bother with Linux because I presume if you’re on Linux you can RTFM, and for Mac, I can google something out but I can’t verify it works so I’m gonna leave that to you.
The API and Domain code are pretty basic, you only need to add the public partial class Program { }
as the last line in your API setup code. This will enable you to create something (Program) to inject around (see here).
Since the creation of the container takes a while (~10 seconds on my machine), we only want to create the container once and keep it running. Test frameworks create a completely new instance of context for every test suite, so we have to somehow circumvent that.
internal static string? ConnectionString { get; private set; }
private const string TestDatabase = "Test";
[ModuleInitializer]
internal static void InitializeContainer()
{
var container = new ContainerBuilder<MsSqlContainer>()
.ConfigureDockerImageName("mcr.microsoft.com/mssql/server:2022-latest")
.ConfigureDatabaseConfiguration("sa", "Password123#", TestDatabase)
.Build();
container.StartAsync().Wait();
ConnectionString = container.GetConnectionString(TestDatabase) +
";TrustServerCertificate=True";
}
ModuleInitializerAttribute attribute
The ModuleInitializerAttribute attribute provides additional runtime guarantees to methods. For example, the marked methods are guaranteed to run before any other method is invoked or any field is accessed in the whole module.
This is exactly what we need! The ContainerBuilder and MsSqlContainer come from TestContainers.Container.Database.MsSql nuget with GitHub repo here.
You can choose the exact Docker image you want, while the username is not important. Whatever you put in as a username, it’s going to be set on sa
, while you can control the password and the name. Since we’re not sure what port the database is going to be on, we will get the connection string directly from the container once it starts.
Testing with databases – code
Since there is some code we’re going to be reusing for each test suite, I’ve dumped it into a base class.
public abstract class IntegrationTestBase :
IClassFixture<WebApplicationFactory<Program>>
{
public HttpClient Client { get; }
public ApplicationContext DbContext { get; }
protected IntegrationTestBase(WebApplicationFactory<Program> factory)
{
factory = factory
.WithWebHostBuilder(builder =>
{
var integrationConfig = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
integrationConfig.GetSection("ConnectionStrings")
["DefaultConnection"] =
ContainerInitializer.ConnectionString;
builder.UseConfiguration(integrationConfig);
});
Client = factory.CreateClient();
DbContext = factory.Services.CreateScope()
.ServiceProvider
.GetService<ApplicationContext>()!;
DbContext!.Database.EnsureDeleted();
DbContext!.Database.EnsureCreated();
}
protected async Task InsertData(IEnumerable<Person> people)
{
await DbContext.People.AddRangeAsync(people);
await DbContext.SaveChangesAsync();
}
}
We’re using the Program which we added at the end of our API startup. After that, add the app settings to the configuration and then override the connection string with the one we got from our Docker.
We then create the HTTP client from the factory and get the database context. Make sure that the database is deleted and do a cleanup between the tests, before recreating it.
There is also a helper method called InsertData
that is used to, well, insert the data into the database.
Tests
And now our tests that are using a real database look fairly nice! I’m leaving out the data insertion to get the point across better.
var people = await Client.GetAsync("/people?filter=Lorem");
people.StatusCode.Should().Be(System.Net.HttpStatusCode.OK);
var content = await people.Content.ReadAsStringAsync();
var responseData = JsonConvert.DeserializeObject<IEnumerable<Person>>(content);
responseData.Count().Should().Be(1);
Conclusion
It has always been a pain in the neck for me to write tests where I had a database layer because of mocking the layers, and maintaining the state if the test needed to do more than one CRUD operation, and without such tests, you had to fall back to manual testing or running postman tests once deployed to test environment.
This approach enabled me to write easy tests to see if the application is actually working when we add all the parts together, and not just that individual parts are working as intended, and it’s my hope it will help you write better tests and as a corollary better applications.