DEV Community

Cover image for Is there a better way of writing Integration Tests in using containers .NET?
Alex Kondrashov
Alex Kondrashov

Posted on

Is there a better way of writing Integration Tests in using containers .NET?

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The full code is here.

We can now run the integration test via docker-compose up -build -abort-on-container-exit.

Console output when running tests using docker-compose

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();
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

This approach allows us to run tests using dotnet test command, which is a plus:

Console output when running tests using TestContainer

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

  1. Repository with the integration test using Docker Compose.
  2. Repository with the integration test using TestContainers.

Top comments (1)

Collapse
 
billie34 profile image
Billie

I think this is a good suggestion, I will try it and give my mini crossword opinion