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