← назад к разделу

Чем больше кода, тем труднее понять, каких тестов писать больше, каких меньше и где проходит граница между уровнями. Пирамида тестов — простая модель, которая расставляет всё по местам.

Зачем нужна пирамида

Без модели команды часто пишут либо только 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, слайсы и конфигурация контекста.