Косвенное тестирование (Indirect Testing)

Тест обращается к целевому объекту не напрямую, а через фасад, обёртку или другой объект. Причинно-следственная связь между действием теста и проверяемым компонентом неочевидна.

Симптомы

  • Тест OrderServiceTest вызывает appFacade.processOrder(...) вместо orderService.calculateTotal(...)
  • Используется «удобная» утилита-обёртка вместо тестируемого класса напрямую
  • При падении непонятно, в каком именно компоненте за фасадом произошла ошибка
  • Тест-класс называется XxxTest, но создаёт и вызывает ApplicationFacade или ServiceLocator

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

  • Фасад удобен для вызова, а прямой класс требует настройки зависимостей
  • Нет DI-контейнера или фабрики — сложно создать OrderService с нужными зависимостями вручную
  • Тест писался «по аналогии» с интеграционным тестом, а не как unit-тест
  • Незнание паттернов stub/mock для изоляции зависимостей

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

  • Неясная диагностика: фасад может делегировать в 5 разных сервисов — при падении неизвестно, кто виноват
  • Избыточная связанность: тест зависит от реализации фасада, а не от контракта целевого класса
  • Слабая изоляция: изменение несвязанного компонента за фасадом ломает «тест OrderService»
  • Маскировка дефектов: фасад может компенсировать баг в сервисе, и тест не обнаружит проблему

Решение

Тест работает напрямую с тестируемым классом, зависимости изолированы через моки:

// ❌ через фасад — неясно, что именно тестируется
BigDecimal total = appFacade.processOrder("user1", "product1", 3);
assertEquals(new BigDecimal("300.00"), total);

// ✅ напрямую с тестируемым классом
OrderService orderService = new OrderService(priceRepository);
Order order = new Order("product1", 3, new BigDecimal("100.00"));
BigDecimal total = orderService.calculateTotal(order);
assertThat(total).isEqualByComparingTo("300.00");