Test Hook (Test Hook)

В продакшн-коде появляются ветки if (testMode), специальные сеттеры или флаги, существующие исключительно для того, чтобы тест мог изменить поведение системы. Продакшн-код «знает» о существовании тестов.

Симптомы

  • Поле private boolean testMode или private boolean isTest в бизнес-классе
  • Публичный метод setTestMode(true) или enableTestBehavior() в продакшн-классе
  • if (System.getProperty("test") != null) в продакшн-коде
  • Конструктор с параметром boolean forTesting в бизнес-логике
  • Комментарий // только для тестов рядом с методом в продакшн-классе

Причины возникновения

  • Нет DI-контейнера или интерфейсов для замены зависимостей — разработчик добавляет флаг как быстрое решение
  • Унаследованный код с жёсткими зависимостями, которые трудно подменить
  • Нежелание рефакторить архитектуру ради тестируемости
  • «Быстро добавлю флаг, потом уберу» — флаг остаётся навсегда

Почему это проблема

  • Тест проверяет другой код: в режиме testMode выполняется другая ветка, а не та, что работает в продакшне
  • Загрязнение продакшн-кода: бизнес-логика содержит тестовую инфраструктуру, нарушается SRP
  • Риск в продакшне: флаг может случайно активироваться (env-переменная, конфигурация) в реальной среде
  • Ложное покрытие: тест показывает coverage, но проверяет не то, что реально выполняется в продакшне

Решение

Dependency Injection и интерфейсы — заменять зависимости через конструктор, а не через флаги:

// ❌ флаг testMode в продакшн-коде
public PaymentResult charge(Order order) {
    if (testMode) return PaymentResult.success("FAKE-TXN-ID");
    return gateway.charge(order.getAmount(), order.getCard());
}

// ✅ интерфейс + DI, в тесте подставляем стаб
public interface PaymentGateway { PaymentResult charge(BigDecimal amount, CardDetails card); }

// Тест: мок через Mockito — продакшн-код не изменён
PaymentGateway stub = mock(PaymentGateway.class);
when(stub.charge(any(), any())).thenReturn(PaymentResult.success("TXN-123"));
PaymentService service = new PaymentService(stub);