Опубликовано
- 2 мин чтения
Упрощаем интеграционные тесты с Testcontainers

Интеграционные тесты играют важную роль в разработке. Они позволяют увидеть, как система работает с внешними зависимостями, например, базами данных. Чтобы запустить такой тест, нужно где-то создать базу данных — например, на виртуальной машине или на локальной машине. Однако лучше использовать фреймворк Testcontainers.
Что такое Testcontainers?
Testcontainers — это библиотека, которая предоставляет удобные API для запуска настоящих сервисов в контейнерах Docker для локальной разработки и тестирования. С помощью Testcontainers можно писать тесты, которые используют те же сервисы, что и в продакшене, без моков и встроенных реализаций (источник).
Пример использования
Рассмотрим класс UserInfoService
. Сервис сначала ищет UserInfo
в кэше. Если не находит, обращается к базе данных. Если и там нет, возвращает null
.
public record UserInfo(string Id, string Name, string Address);
public class UserInfoService(IRepository _repository)
{
private readonly ConcurrentDictionary<string, UserInfo> _cache = new();
public async Task<UserInfo?> Get(string id, CancellationToken ct)
{
if (_cache.TryGetValue(id, out var userInfo)) return userInfo;
userInfo = await _repository.Get(id, ct);
if (userInfo is not null)
{
_cache.TryAdd(id, userInfo);
return userInfo;
}
return null;
}
}
Теперь задача: если ключа нет в кэше, сервис должен обратиться к базе. Зная это, можно написать интеграционные тесты с Testcontainers. Но для этого нам также нужны реализация IRepository
и миграции.
Реализация репозитория
Используем PostgreSQL. Добавляем зависимости:
dotnet add package Npgsql --version 8.0.2
И добавляем Dapper для упрощения работы с базой:
dotnet add package Dapper --version 2.1.28
Реализация репозитория:
public interface IRepository
{
Task<UserInfo?> Get(string id, CancellationToken ct);
Task Add(UserInfo userInfo);
}
public class Repository(string _connectionString) : IRepository
{
private readonly NpgsqlConnection connection = new(_connectionString);
private readonly string _selectQuery = @"SELECT ""Id"", ""Name"", ""Address"" FROM ""Users"" WHERE ""Id"" = @Id";
private readonly string _insertQuery = @"INSERT INTO ""Users"" (""Id"", ""Name"", ""Address"") VALUES (@Id, @Name, @Address)";
public async Task Add(UserInfo userInfo)
{
await connection.ExecuteAsync(_insertQuery, new { userInfo.Id, userInfo.Name, userInfo.Address });
}
public async Task<UserInfo?> Get(string id, CancellationToken ct)
{
return await connection.QueryFirstOrDefaultAsync<UserInfo>(_selectQuery, new { Id = id });
}
}
Миграции базы данных
Для миграций будем использовать FluentMigrator:
dotnet add package FluentMigrator --version 5.0.0
dotnet add package FluentMigrator.Runner.Postgres --version 5.0.0
Первая миграция создаёт таблицу Users
:
[Migration(20240212, "Создание таблицы Users")]
public class InitialMigration : Migration
{
public override void Up()
{
Create.Table("Users")
.WithColumn("Id").AsString().PrimaryKey()
.WithColumn("Name").AsString()
.WithColumn("Address").AsString();
}
public override void Down()
{
Delete.Table("Users");
}
}
Запуск миграции:
public static class Migrator
{
public static void MigrateUp(string connectionString)
{
var serviceProvider = new ServiceCollection()
.AddFluentMigratorCore()
.ConfigureRunner(rb => rb
.AddPostgres()
.WithGlobalConnectionString(connectionString)
.ScanIn(typeof(InitialMigration).Assembly).For.Migrations())
.AddLogging(lb => lb.AddFluentMigratorConsole())
.BuildServiceProvider(false);
var migrationRunner = serviceProvider.GetRequiredService<IMigrationRunner>();
migrationRunner.MigrateUp();
}
}
Интеграционные тесты
Создадим новый проект для MSTest:
dotnet new mstest
Добавим пакет для Testcontainers PostgreSQL:
dotnet add package Testcontainers.PostgreSql
Код теста:
[TestClass]
public class IntegrationTests
{
private static readonly PostgreSqlContainer _postgres = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
[ClassInitialize]
public static Task Init(TestContext _) => _postgres.StartAsync();
[ClassCleanup]
public static Task Cleanup() => _postgres.DisposeAsync().AsTask();
[TestMethod]
public async Task TestMethod()
{
var connectionString = _postgres.GetConnectionString();
Migrator.MigrateUp(connectionString);
var repository = new Repository(connectionString);
var expected = new UserInfo("1", "John Doe", "Some address");
await repository.Add(expected);
var sut = new UserInfoService(repository);
var actual = await sut.Get("1", CancellationToken.None);
Assert.IsNotNull(actual);
Assert.AreEqual(expected.Id, actual.Id);
Assert.AreEqual(expected.Name, actual.Name);
Assert.AreEqual(expected.Address, actual.Address);
}
}
Вывод
Вот и всё. Как видно, базовое использование Testcontainers для интеграционных тестов очень простое.