Running Unit Tests in Docker
Tags: csharp, unittest, docker, github
Published: 2023-09-24 12:28:16

Hello everyone. In this article, I'll show you how to run unit tests more quickly and conveniently. We won't create an application with some logic; we'll make only a project affiliated with unit tests.

Let's create a console application:

```bash
dotnet new console -n TestsInDockerSample
```
The `Program.cs` class you can delete, it no need. We'll begin by adding NuGet packages, and you should add all of these:

```xml
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0-preview-23424-02" />
<PackageReference Include="Npgsql" Version="8.0.0-preview.4" />
<PackageReference Include="Testcontainers.PostgreSql" Version="3.5.0" />
<PackageReference Include="xunit" Version="2.5.2-pre.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.2-pre.3">
```

The `JetBrains.Annotations` package isn't mandatory, and it needed more for IDE and convenient debugging.
For the implementation, we'll start with adding a fixture class. Please add `DbFixture.cs` class to the root folder of your project. Also, paste this code, and I'll explain what is going on there:

```C#
public sealed class DbFixture : IAsyncLifetime
{
    private readonly INetwork _network = new NetworkBuilder().Build();

    private readonly PostgreSqlContainer _postgreSqlContainer;

    private readonly IContainer _flywayContainer;

    public DbFixture()
    {
        _postgreSqlContainer = new PostgreSqlBuilder()
            .WithImage("postgres:latest")
            .WithNetwork(_network)
            .WithNetworkAliases(nameof(_postgreSqlContainer))
            .Build();

        _flywayContainer = new ContainerBuilder()
            .WithImage("flyway/flyway:latest")
            .WithResourceMapping("migrate/", "/flyway/sql/")
            .WithCommand("-url=jdbc:postgresql://" + nameof(_postgreSqlContainer) + "/")
            .WithCommand("-user=" + PostgreSqlBuilder.DefaultUsername)
            .WithCommand("-password=" + PostgreSqlBuilder.DefaultPassword)
            .WithCommand("-connectRetries=3")
            .WithCommand("migrate")
            .WithNetwork(_network)
            .DependsOn(_postgreSqlContainer)
            .WithWaitStrategy(Wait.ForUnixContainer().AddCustomWaitStrategy(new MigrationCompleted()))
            .Build();
    }

    public DbConnection DbConnection => new NpgsqlConnection(((PostgreSqlContainer)_postgreSqlContainer).GetConnectionString());

    public Task InitializeAsync()
    {
        return _flywayContainer.StartAsync();
    }

    public Task DisposeAsync()
    {
        return Task.CompletedTask;
    }

    private sealed class MigrationCompleted : IWaitUntil
    {
        public Task<bool> UntilAsync(IContainer container)
        {
            return Task.FromResult(TestcontainersStates.Exited.Equals(container.State));
        }
    }
```
So, let's figure it out. As you may have noticed, the main class is derived from `IAsyncLifetime`. This is part of XUnit. This interface contains only two methods that need to be implemented:

```C#
public interface IAsyncLifetime
{
    Task InitializeAsync();
    Task DisposeAsync();
}
```

`InitializeAsync` is responsible for initializing something. In our case, this is launching a container. And upon completion, we clean the resources. We use two containers. The first is the Postgres database, and the second is the Flyway container that performs the migration. As you might guess, this class waits for the migration to complete.

In the next step, you should create a new folder called “migrations”. And put it in these SQL commands. They need migration and seeding at the same time.

```SQL
CREATE TABLE users
(
    id         SERIAL PRIMARY KEY,
    username   VARCHAR(50) NOT NULL,
    email      VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE users ADD COLUMN age INT;
INSERT INTO users (username, email, age) VALUES ('john_doe', 'john@example.com', 30);
```
And another remark: go to your `cproj` file and add these rows. It needs to detect this folder.

```xml
<ItemGroup>
    <None Include="migrate/*.sql" CopyToOutputDirectory="PreserveNewest"/>
</ItemGroup>
```

And finally, let's add a test. You need to create a new class named UserTests. Paste this code:

```C#
public sealed class UserTests : IClassFixture<DbFixture>, IDisposable
{
    private readonly DbConnection _dbConnection;

    public UserTests(DbFixture db)
    {
        _dbConnection = db.DbConnection;
        _dbConnection.Open();
    }

    public void Dispose()
    {
        _dbConnection.Dispose();
    }

    [Fact]
    public void UsersTableContainsJohnDoe()
    {
        // Arrange
        using var command = _dbConnection.CreateCommand();
        command.CommandText = "SELECT username, email, age FROM users;";

        // Act
        using var dataReader = command.ExecuteReader();

        // Assert
        Assert.True(dataReader.Read());
        Assert.Equal("john_doe", dataReader.GetString(0));
        Assert.Equal("john@example.com", dataReader.GetString(1));
        Assert.Equal(30, dataReader.GetInt32(2));
    }
}
```
If you run this simple test, you can see how Docker creates new images with the database, and after finishing the test, they are disposed of.

And as a bonus, let's add the pipeline for GitHub Actions.

```yaml
name: .NET

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Setup .NET
      uses: actions/setup-dotnet@v3
      with:
        dotnet-version: 7.0.x
    - name: Restore dependencies
      run: dotnet restore
    - name: Build
      run: dotnet build --no-restore
    - name: Test
      run: dotnet test --no-build --verbosity normal
```

If your action passed successfully, then go to GitHub and check this out:


![result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z65ucj2lplul94onpng0.png)

The test was passed, and it took 33 seconds. For this test, it is more than enough. However, tests run automatically without your attending.

That's all. Have a nice day, and happy coding.

Source code: [HERE](https://github.com/SergKorol/TestsInDockerSample.git)

<a href="https://www.buymeacoffee.com/b9z99kntvtQ" target="_blank"><img src="http://dartfordwaffler.co.uk/wp-content/uploads/2021/03/buy-me-a-beer.jpg" alt="Buy Me A Beer" style="height: 60px !important;width: 317px !important;" ></a>