Интеграционные тесты прямо в CI/CD - максимум уверенности в коде 🚀
Я запускаю интеграционные тесты прямо внутри CI/CD пайплайна.
Так я проверяю не абстракции и моки, а реальное поведение системы.
Всё это возможно благодаря:
- Docker
- Testcontainers
Идея простая:
ты поднимаешь реальные внешние сервисы как контейнеры и используешь их прямо в тестах.
В примере поднимаются:
- PostgreSQL
- Redis
- Keycloak
Контейнеры:
- автоматически стартуют перед тестами
- доступны из кода приложения
- уничтожаются после выполнения тестов
Почему это работает лучше моков:
- тесты максимально близки к продакшену
- одинаковое поведение локально и в CI
- сразу ловятся проблемы с конфигурацией, миграциями, auth
- меньше сюрпризов после деплоя
Особенно полезно для:
- backend-сервисов
- микросервисной архитектуры
- систем с авторизацией и внешними зависимостями
- .NET-приложений с серьёзной бизнес-логикой
Если хочешь вывести интеграционное тестирование в .NET на новый уровень — Testcontainers стоит попробовать обязательно.
#dotnet, #testing
Я запускаю интеграционные тесты прямо внутри CI/CD пайплайна.
Так я проверяю не абстракции и моки, а реальное поведение системы.
Всё это возможно благодаря:
- Docker
- Testcontainers
Идея простая:
ты поднимаешь реальные внешние сервисы как контейнеры и используешь их прямо в тестах.
В примере поднимаются:
- PostgreSQL
- Redis
- Keycloak
Контейнеры:
- автоматически стартуют перед тестами
- доступны из кода приложения
- уничтожаются после выполнения тестов
Почему это работает лучше моков:
- тесты максимально близки к продакшену
- одинаковое поведение локально и в CI
- сразу ловятся проблемы с конфигурацией, миграциями, auth
- меньше сюрпризов после деплоя
Особенно полезно для:
- backend-сервисов
- микросервисной архитектуры
- систем с авторизацией и внешними зависимостями
- .NET-приложений с серьёзной бизнес-логикой
Если хочешь вывести интеграционное тестирование в .NET на новый уровень — Testcontainers стоит попробовать обязательно.
#dotnet, #testing
🐳 «Используй Testcontainers вместо in-memory» - это только половина правды
Все уже выучили: EF Core InMemory provider - не интеграционный тест.
Он не ловит:
- баги в LINQ-трансляции
- ограничения БД
- коллации
- реальные типы колонок
- поведение конкретного SQL-провайдера
Окей, заменили на реальный PostgreSQL через Testcontainers. Победа? Не совсем.
Вот что начинается дальше.
1. Вы получили «медленное враньё» вместо «быстрого»
Поднимать контейнер на каждый тест-класс - быстрый способ превратить CI из 30 секунд в 8 минут.
Нормальный вариант:
- один контейнер на всю тестовую сессию
- изоляция данных между тестами через Respawn
- без пересоздания базы и контейнера каждый раз
Respawn чистит таблицы с учётом графа foreign keys за миллисекунды.
2. Транзакционный откат ≠ реальный сценарий
Трюк «обернули тест в транзакцию и откатили» красиво выглядит, но ломается, когда в коде есть:
- свои транзакции
- несколько SaveChanges
- фоновые операции
- поведение, завязанное на commit
В итоге тестируется сценарий, которого в проде нет.
3. Самая коварная ловушка - общий DbContext
Если тест и код используют один экземпляр DbContext, EF может вернуть данные из change tracker, а не из базы.
Тест зелёный, но он врёт: реальный SQL-запрос мог вообще не выполниться.
Между Act и Assert стоит чистить трекер:
4. Бонус, который теряют 90% команд - тест миграций
Реальная БД позволяет прогнать EF-миграции на чистой схеме.
Если миграция падает или схема разъехалась с моделью, вы узнаёте об этом в CI, а не в проде в пятницу вечером.
Пример базового подхода:
Testcontainers - это не галочка «best practice», а смена философии.
Без нормальной изоляции данных вы просто пересели с быстрого вранья на медленное.
А как вы изолируете состояние БД между интеграционными тестами - Respawn, транзакции или пересоздание контейнера?
#dotnet #csharp #testing #efcore
Все уже выучили: EF Core InMemory provider - не интеграционный тест.
Он не ловит:
- баги в LINQ-трансляции
- ограничения БД
- коллации
- реальные типы колонок
- поведение конкретного SQL-провайдера
Окей, заменили на реальный PostgreSQL через Testcontainers. Победа? Не совсем.
Вот что начинается дальше.
1. Вы получили «медленное враньё» вместо «быстрого»
Поднимать контейнер на каждый тест-класс - быстрый способ превратить CI из 30 секунд в 8 минут.
Нормальный вариант:
- один контейнер на всю тестовую сессию
- изоляция данных между тестами через Respawn
- без пересоздания базы и контейнера каждый раз
Respawn чистит таблицы с учётом графа foreign keys за миллисекунды.
2. Транзакционный откат ≠ реальный сценарий
Трюк «обернули тест в транзакцию и откатили» красиво выглядит, но ломается, когда в коде есть:
- свои транзакции
- несколько SaveChanges
- фоновые операции
- поведение, завязанное на commit
В итоге тестируется сценарий, которого в проде нет.
3. Самая коварная ловушка - общий DbContext
Если тест и код используют один экземпляр DbContext, EF может вернуть данные из change tracker, а не из базы.
Тест зелёный, но он врёт: реальный SQL-запрос мог вообще не выполниться.
Между Act и Assert стоит чистить трекер:
Db.ChangeTracker.Clear();
4. Бонус, который теряют 90% команд - тест миграций
Реальная БД позволяет прогнать EF-миграции на чистой схеме.
Если миграция падает или схема разъехалась с моделью, вы узнаёте об этом в CI, а не в проде в пятницу вечером.
Пример базового подхода:
public class IntegrationTestBase : IAsyncLifetime
{
private static readonly PostgreSqlContainer _db =
new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
private Respawner _respawner = null!;
protected AppDbContext Db = null!;
public async Task InitializeAsync()
{
await _db.StartAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(_db.GetConnectionString())
.Options;
Db = new AppDbContext(options);
// Реальные миграции - заодно проверяем, что они накатываются
await Db.Database.MigrateAsync();
await using var conn = new NpgsqlConnection(_db.GetConnectionString());
await conn.OpenAsync();
_respawner = await Respawner.CreateAsync(conn, new RespawnerOptions
{
DbAdapter = DbAdapter.Postgres,
SchemasToInclude = ["public"]
});
}
// Сброс данных перед каждым тестом - без пересоздания контейнера
protected async Task ResetAsync()
{
await using var conn = new NpgsqlConnection(_db.GetConnectionString());
await conn.OpenAsync();
await _respawner.ResetAsync(conn);
// Иначе тест может читать из кеша, а не из БД
Db.ChangeTracker.Clear();
}
public Task DisposeAsync() => Task.CompletedTask;
}
Testcontainers - это не галочка «best practice», а смена философии.
Без нормальной изоляции данных вы просто пересели с быстрого вранья на медленное.
А как вы изолируете состояние БД между интеграционными тестами - Respawn, транзакции или пересоздание контейнера?
#dotnet #csharp #testing #efcore
💡 C#: заставьте архитектуру ломать CI, если кто-то нарушил правила
Компилятор C# отлично проверяет типы, но ему всё равно, что
Code Review тоже не гарантирует, что такое заметят.
Для этого можно использовать Architecture Tests — тесты, которые проверяют не бизнес-логику, а структуру проекта.
Например, через
Теперь случайный:
сломает CI так же, как обычный упавший unit-тест.
Можно зафиксировать и naming convention:
Или заставить сервисы быть
Особенно полезно это становится в больших Clean Architecture / DDD / Modular Monolith проектах.
Вместо документа:
получаем исполняемое правило:
По сути, архитектура превращается из договорённости команды в часть автоматической проверки проекта.
#CSharp #DotNet #Architecture #Testing
Компилятор C# отлично проверяет типы, но ему всё равно, что
Application внезапно начал зависеть от Infrastructure.Code Review тоже не гарантирует, что такое заметят.
Для этого можно использовать Architecture Tests — тесты, которые проверяют не бизнес-логику, а структуру проекта.
Например, через
NetArchTest.Rules:
[Fact]
public void Application_Should_Not_Depend_On_Infrastructure()
{
var result = Types
.InAssembly(ApplicationAssembly)
.ShouldNot()
.HaveDependencyOn("MyApp.Infrastructure")
.GetResult();
Assert.True(result.IsSuccessful);
}
Теперь случайный:
Application → Infrastructure
сломает CI так же, как обычный упавший unit-тест.
Можно зафиксировать и naming convention:
[Fact]
public void Handlers_Should_End_With_Handler()
{
var result = Types
.InAssembly(ApplicationAssembly)
.That()
.ImplementInterface(typeof(ICommandHandler<>))
.Should()
.HaveNameEndingWith("Handler")
.GetResult();
Assert.True(result.IsSuccessful);
}
Или заставить сервисы быть
sealed:
Types
.InAssembly(ApplicationAssembly)
.That()
.HaveNameEndingWith("Service")
.Should()
.BeSealed();
Особенно полезно это становится в больших Clean Architecture / DDD / Modular Monolith проектах.
Вместо документа:
❌ «Application не должен зависеть от Infrastructure»
получаем исполняемое правило:
✅ нарушил архитектуру → тест упал → PR не прошёл
По сути, архитектура превращается из договорённости команды в часть автоматической проверки проекта.
#CSharp #DotNet #Architecture #Testing