Is there a better way of writing Integration Tests in using containers .NET?
Alex Kondrashov
Posted on December 11, 2022
Too Long to Read
The cleanest way of writing integration tests is using throwaway containers. TestContainers library helps you to easily manage it.
More…
Managing dependencies for automated tests can be challenging. We could spend hours and manage it manually, or we could automate it and use third party libraries to manage it. In this article I'm searching for a clean and easy way of writing integration test for a modern ASP.NET web application.
System under test
My example exposes a REST API to Create and Get cars:
[HttpPost]
public async Task<CarModel> Create(CarModel model)
{
using (var connection = await _dbConnectionFactory.CreateConnectionAsync())
{
var result = await connection.QueryAsync<string>("INSERT INTO cars (name, available) values (@Name, @Available); SELECT LAST_INSERT_ID();", model);
var id = result.Single();
return await Get(id);
}
}
[HttpGet("{id}")]
public async Task<CarModel> Get(string id)
{
using (var connection = await _dbConnectionFactory.CreateConnectionAsync())
{
var result = await connection.QueryAsync<CarModel>("SELECT id,available,name FROM cars WHERE id=@Id", new { Id = id });
var model = result.FirstOrDefault();
return model;
}
}
You can find the full code here.
Staring with an integration test
🟢 ⚪ ⚪
Integration test - is a type of software testing in which the different units, modules or components of a software application are tested as a combined entity.
How do we know that we write an integration test? We should call all layers of our system from top to bottom in our test: REST API, Controller, Service, Repository and Database.
[Fact]
public async void testCreateCar()
{
var client = _factory.CreateClient();
var car = new CarModel { Available = false, Name = "Test Text" };
var result = await client.PostAsync(_endpoint, new StringContent(JsonConvert.SerializeObject(car), Encoding.UTF8, "application/json"));
var expectedModel = JsonConvert.DeserializeObject<CarModel>(await result.Content.ReadAsStringAsync());
var response = await client.GetAsync($"{_endpoint}/{expectedModel.Id}");
var actualModel = JsonConvert.DeserializeObject<CarModel>(await response.Content.ReadAsStringAsync());
Assert.Equal(expectedModel.Id, actualModel.Id);
Assert.Equal(expectedModel.Name, actualModel.Name);
}
If we run REST API and the database prior running the test - it would pass. We have a proof that our system works now! Any integration test is better than the absence of it.
DIY throwaway container using Docker
🟢 🟢 ⚪
What are the problems of the test above? There is a lot of manual labour involved into running it. We need to manage the database and the web application manually.
We would want to spin up dependencies when we start test and tear it down when the execution is over. Docker should be able to help with this. We can lay it down using 3 containers: web application
, database
and integration container
that will run the integration test. All integration between containers is coded in docker-compose.yml
:
version: '3'
services:
integration:
build:
context: .
dockerfile: Dockerfile.integration
environment:
- API_URL=http://web:5001
- CONNECTION_STRING=Server=db;Database=carsdb;Uid=root;Pwd=password;SslMode=Required;
entrypoint: bash /app/wait_for_it.sh web:5001 -t 0 -- dotnet test --logger "console;verbosity=detailed"
depends_on:
- web
- db
web:
build: .
ports:
- 5001:5001
environment:
- ASPNETCORE_ENVIRONMENT=Development
- CONNECTION_STRING=Server=db;Database=carsdb;Uid=root;Pwd=password;SslMode=Required;
entrypoint: bash /app/wait_for_it.sh db:3306 -t 0 -- dotnet /app/Cars.dll
depends_on:
- db
db:
platform: linux/x86_64
image: mysql
ports:
- 3307:3306
# Start the container with a carsdb, and password as the root users password
environment:
- MYSQL_DATABASE=carsdb
- MYSQL_ROOT_PASSWORD=password
# Volume the scripts folder over that we setup earlier.
volumes:
- ./Scripts:/docker-entrypoint-initdb.d
The full code is here.
We can now run the integration test via docker-compose up -build -abort-on-container-exit
.
TestContainers to manage throwaway containers
🟢 🟢 🟢
Testcontainers for .NET is a library to support tests with throwaway instances of Docker containers for all compatible .NET Standard versions. The library is built on top of the .NET Docker remote API and provides a lightweight implementation to support your test environment in all circumstances.
This library gives a clean way to write Integration Tests if we don't want to manage Docker containers ourselves.
We can use a pre-defined MySqlTestcontainer
for our web application. The configuration of TestContainer is done via WebApplicationFactory
class:
public class IntegrationTestFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
private readonly TestcontainerDatabase _container;
public IntegrationTestFactory()
{
_container = new TestcontainersBuilder<MySqlTestcontainer>()
.WithDatabase(new MySqlTestcontainerConfiguration
{
Password = "localdevpassword#123",
Database = "carsdb",
})
.WithImage("mysql:latest")
.WithCleanUp(true)
.Build();
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.RemoveAll<IDbConnectionFactory>();
services.AddSingleton<IDbConnectionFactory>(_ => new MySqlConnectionFactory(_container.ConnectionString));
});
}
public async Task InitializeAsync()
{
await _container.StartAsync();
await _container.ExecScriptAsync("CREATE TABLE IF NOT EXISTS cars (id SERIAL, name VARCHAR(100), available BOOLEAN)");
}
public new async Task DisposeAsync() => await _container.DisposeAsync();
}
Thanks to IAsyncLifetime
we can initialize and dispose our database using InitializeAsync() and DisposeAsync() methods.
We now need to use IntegrationTestFactory
to get a fake HttpClient
:
[Fact]
public async void testCreateCar()
{
var client = _factory.CreateClient();
var car = new CarModel { Available = false, Name = "Test Text" };
var result = await client.PostAsync(_endpoint, new StringContent(JsonConvert.SerializeObject(car), Encoding.UTF8, "application/json"));
var expectedModel = JsonConvert.DeserializeObject<CarModel>(await result.Content.ReadAsStringAsync());
var response = await client.GetAsync($"{_endpoint}/{expectedModel.Id}");
var actualModel = JsonConvert.DeserializeObject<CarModel>(await response.Content.ReadAsStringAsync());
Assert.Equal(expectedModel.Id, actualModel.Id);
Assert.Equal(expectedModel.Name, actualModel.Name);
}
This approach allows us to run tests using dotnet test
command, which is a plus:
Look at this! We've run our integration test in Docker without having to manually manage containers.
Summary
We have written an integration test and all we need to do to run it is to launch Docker and execute dotnet test
command. The web server and the database will be span up for us by TestContainers library. Let me know in the comments below if you see any drawbacks in this.
Resources
- Repository with the integration test using Docker Compose.
- Repository with the integration test using TestContainers.
Posted on December 11, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.