Is there a better way of writing Integration Tests in using containers .NET?
Published: 2022-12-11 17:08:52

## 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:


```csharp
[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](https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Falkondrashov%2Fintegration-testing-docker).

##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_.

```csharp
[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`:

```yaml
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](https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Falkondrashov%2Fintegration-testing-docker).

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


![Console output when running tests using docker-compose](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0v0bvz7zlj3u5mgjnhu9.gif)

##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:

```csharp
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`:

```csharp
[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:

![Console output when running tests using TestContainer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o83onfn0n47ats9j8fc3.png)

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](https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Falkondrashov%2Fintegration-testing-docker) with the integration test using Docker Compose.
2. [Repository](https://medium.com/r/?url=https%3A%2F%2Fgithub.com%2Falkondrashov%2Fintegration-testing-testcontainers) with the integration test using TestContainers.