Тестовая Пирамида — Ложь (и Что Делать Вместо Этого). Окончание
Начало
Что делать вместо тестовой пирамиды
Вот структура тестов для типичного сервиса .NET или модульного монолита.
Уровень 1: Тонкая база модульных тестов (15-25% тестов)
Вся логика домена. Никаких контейнеров или моков. Микросекунды на тест. Для этого и существуют модульные тесты. Если модульному тесту нужен мок, то он будет на уровне интеграции.
[Fact]
public void Confirm_WhenPending_TransitionsToConfirmed()
{
var order = Order.Create(
CustomerId.New(), Money.Usd(100));
order.Confirm();
order.Status.Should()
.Be(OrderStatus.Confirmed);
order.DomainEvents.Should()
.ContainSingle(e => e is OrderConfirmedEvent);
}
Уровень 2: Толстый средний слой интеграционных тестов (60-70%)
Все обработчики команд и запросов, все конечные точки HTTP, все потребители сообщений тестируются на реальной инфраструктуре внутри тестовых контейнеров. В модульном монолите именно здесь проверяется корректность взаимодействия модулей друг с другом через их публичные API.
public class DeleteAccountTests(
IntegrationTestWebAppFactory factory)
: BaseIntegrationTest(factory)
{
[Fact]
public async Task DeleteAccount_MarksAccountDeleted()
{
var acc = await CreateAccountAsync();
var resp = await HttpClient
.DeleteAsync($"/accounts/{acc.Id}");
resp.StatusCode.Should().Be(HttpStatusCode.NoContent);
var stored = await DbContext.Accounts
.IgnoreQueryFilters()
.SingleAsync(a => a.Id == acc.Id);
stored.IsDeleted.Should().BeTrue();
}
}
Эти тесты проверяют слой HTTP, маршрутизацию, привязку модели, авторизацию, обработчики, контексты, EF Core и PostgreSQL. Они доказывают то, что вас действительно интересует: когда я вызываю эту конечную точку, строка в базе изменяется. И это происходит без необходимости кому-либо помнить о вызове SaveChangesAsync и проверке, вызывается ли он.
Уровень 3: Небольшое количество сквозных тестов (<10%)
Только те потоки, где скрытый сбой может представлять коммерческую проблему или проблему соответствия требованиям. Регистрация, оплата, возврат средств, сброс пароля, двухфакторная аутентификация. Они медленные и иногда нестабильные, но они выявляют тот единственный режим отказа, который все остальные тесты пропускают: система в целом продолжает работать.
Уровень 0: Архитектурные и контрактные тесты
Часто забываются, но они являются частью набора тестов. Архитектурные тесты обеспечивают соблюдение разделения на слои и границ модулей. Контрактные тесты проверяют, что схемы сообщений и формат API не меняются. Они выполняются за миллисекунды и выявляют ошибки типа «через шесть месяцев кто-то сломает это, не заметив».
Полученная форма больше напоминает ромб, чем пирамиду. Толстая середина — это намеренное решение. Именно отсюда и берётся уверенность в тестах.
Обычные возражения
«Интеграционные тесты медленные». Мой типичный набор интеграционных тестов выполняется за 2-4 минуты в CI с повторным использованием Testcontainers и распараллеливанием тестовых классов. Медленнее, чем модульные тесты? Да. Но быстрее, чем обнаружение ошибки в продакшене.
«Моки хороши, если вы дисциплинированы». Возможно. Но в каждой крупной кодовой базе, которую я проверял и которая сильно полагалась на моки, была одна и та же патология: тесты проходят после рефакторинга, даже если рефакторинг сломал продакшен. Это не проблема дисциплины. Это неправильное использование инструмента.
Итого
Пирамида тестирования была хорошим советом для инфраструктуры 2009 года, но является плохим советом для инфраструктуры 2026 года. Testcontainers и Aspire изменили экономику, и самый быстрый цикл обратной связи, который по-прежнему сообщает правду, теперь — это интеграционный тест с реальными зависимостями. Модульные тесты по-прежнему должны быть сосредоточены на чистой логике предметной области. Всё, что переходит через границы модулей, должно быть на уровне интеграции.
Источник: https://www.milanjovanovic.tech/blog/the-test-pyramid-is-a-lie-and-what-i-do-instead