Чем больше кода, тем труднее понять, каких тестов писать больше, каких меньше и где проходит граница между уровнями. Пирамида тестов — простая модель, которая расставляет всё по местам.
Зачем нужна пирамида
Без модели команды часто пишут либо только unit-тесты (не замечают проблем интеграции), либо только e2e-тесты (медленный обратная связь, хрупкие сьюты). Оба крайних случая дороги в поддержке.
Пирамида говорит: у основания — много быстрых изолированных тестов, на вершине — мало медленных сквозных. Чем выше уровень, тем дороже запуск и сложнее диагностика. Поэтому вверх идём только за тем, что нижний уровень проверить не может.
Три слоя
┌───────┐
│ e2e │ ← мало, медленно
├───────┤
│ integ │ ← умеренно
├───────┤
│ unit │ ← много, быстро
└───────┘
| Уровень | Что проверяет | Скорость |
|---|---|---|
| Unit | один класс / функция, без внешних зависимостей | миллисекунды |
| Integration | несколько компонентов + реальная инфраструктура | секунды |
| E2e | полный путь от UI / API до БД | десятки секунд |
Unit-тесты: изоляция и скорость
Unit-тест проверяет один класс в полной изоляции — без Spring-контекста, без базы, без сети. Зависимости заменяются заглушками.
Короткая формула: один тест — один сценарий поведения.
class DiscountServiceTest {
private final DiscountService service = new DiscountService();
@Test
void appliesDiscountWhenTotalExceedsThreshold() {
var total = service.apply(new Money(1000), CustomerTier.GOLD);
assertThat(total).isEqualTo(new Money(900));
}
@Test
void noDiscountBelowThreshold() {
var total = service.apply(new Money(500), CustomerTier.GOLD);
assertThat(total).isEqualTo(new Money(500));
}
}
Хорошо поддаются unit-тестированию: бизнес-правила, граничные случаи, расчёты, маппинг, валидация. Плохо поддаются: код, сшитый с фреймворком или инфраструктурой — для него нужен следующий уровень.
Integration-тесты: реальная инфраструктура
Integration-тест поднимает часть (или весь) Spring-контекст и работает с реальными зависимостями — базой данных, брокером сообщений, внешним HTTP-сервисом.
@SpringBootTest
@Transactional
class OrderRepositoryIT {
@Autowired
OrderRepository repository;
@Test
void savesAndFindsOrder() {
var order = repository.save(new Order(CustomerId.of("c-1"), List.of()));
assertThat(repository.findById(order.id())).isPresent();
}
}
Для реальной базы в тестах используют Testcontainers — он поднимает PostgreSQL (или другой движок) в Docker-контейнере и останавливает после сьюта. Конфигурация Spring-тестов подробнее описана в разделе Тестирование в Spring.
Integration-тесты медленнее unit, поэтому их пишут на критические пути: репозитории, обработчики команд, HTTP-клиенты к внешним сервисам.
E2e-тесты: полный путь запроса
E2e-тест проходит весь путь: HTTP-запрос → контроллер → бизнес-логика → база → HTTP-ответ. Окружение максимально близко к продакшену.
@SpringBootTest(webEnvironment = RANDOM_PORT)
class CreateOrderE2eTest {
@Autowired
TestRestTemplate http;
@Test
void createsOrderAndReturns201() {
var body = new CreateOrderRequest(customerId, items);
var response = http.postForEntity("/orders", body, Void.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
}
E2e-тесты — самые дорогие: поднимают весь контекст, медленно запускаются, сложно локализовать падение. Их пишут немного — только на ключевые пользовательские сценарии.
Что тестировать, а что нет
Стоит тестировать:
- бизнес-правила и граничные случаи (скидки, лимиты, статусные переходы)
- маппинг данных между слоями
- обработку ошибочных входных данных
- взаимодействие с базой на критических путях
Не стоит тестировать:
- геттеры, сеттеры, конструкторы (очевидный код без логики)
- конфигурацию фреймворка — Spring уже протестирован сам по себе
- детали реализации, которые могут поменяться (внутренние приватные методы)
Правило: тест должен сломаться, когда поведение меняется, и оставаться зелёным при рефакторинге без изменения поведения.
Коротко
- Пирамида: много unit → умеренно integration → мало e2e.
- Unit-тесты — быстрые, изолированные, без Spring-контекста; покрывают бизнес-логику.
- Integration-тесты — с реальной инфраструктурой (
Testcontainers,@SpringBootTest); для репозиториев и HTTP-клиентов. - E2e-тесты — полный путь запроса; пишут только на ключевые сценарии.
- Тестируют поведение и бизнес-правила, не геттеры и не фреймворк.
- Чем выше уровень, тем дороже запуск и тем меньше тестов нужно.
Что почитать дальше
- Integration-тесты в деталях —
Testcontainers, транзакции, изоляция тестовых данных. - Моки и внешние зависимости — когда использовать
WireMock, а когда реальную заглушку. - Тестирование в Spring —
@WebMvcTest,@DataJpaTest, слайсы и конфигурация контекста.