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);