ConstructiCat Logo
CodeBust.
Browse section ▾

Тестовая логика в продакшен-коде.

Продакшен-код содержит логику, ветви или члены, существующие лишь для поддержки тестирования, что размывает грань между тем, что попадает в продакшен, и тем, что просто тестируется.

##Signs and Symptoms

Вы находите в тестируемой системе (SUT) код, который имеет значение, только когда выполняется тест. Характерные признаки:

  • Тестовые хуки / флаги режима — ветви, завязанные на флаг testing, isTest, NODE_ENV === 'test' или mock, которые замыкают накоротко настоящее поведение.
  • Члены «только для тестов» — публичные сеттеры, геттеры, reset() или конструкторы, добавленные исключительно для того, чтобы тест мог добраться до внутреннего состояния, нередко помеченные @VisibleForTesting / @TestOnly, а затем реально вызываемые из продакшена.
  • Загрязнение сравнением — логика equals() / сравнения, добавленная в продакшен-класс лишь для того, чтобы проверка могла сравнить два объекта.
  • Зависимость от тестов в продакшене — продакшен-модули, импортирующие тестовый фреймворк, фикстуру или фабрику моков.
// ЗАПАХ: поставляемый код ведёт себя иначе, когда включён «testing»
class PaymentService {
  charge(order: Order) {
    if (process.env.NODE_ENV === 'test' || this.isTesting) {
      return { status: 'ok', id: 'FAKE-TEST-ID' }; // реальный шлюз никогда не задействуется
    }
    return this.gateway.charge(order); // <-- путь, который реально уходит в продакшен
  }
}

«Настоящая» ветвь — это та, в которую попадают ваши клиенты, и именно её пропускают ваши тесты.

##Reasons for the Problem

Почему это происходит

  • Тестируемую систему трудно тестировать (она обращается к сети, часам, платёжному шлюзу или файловой системе), и добавить заглушку if (testing) быстрее, чем ввести нормальный шов.
  • Тесту нужно наблюдать или задавать внутреннее состояние, и разработчик открывает к нему доступ «только ради теста».
  • Создавать заглушки/моки неудобно, поэтому подставные данные жёстко зашиты за флагом.

Чем это вредит

  • Ложная уверенность. Тесты прогоняют только тестовую ветвь, поэтому продакшен-путь уходит в релиз непроверенным. Зелёные тесты доказывают, что работает подделка, а не настоящее поведение.
  • Надёжность / безопасность. Тестовый код, доживший до продакшена, может сработать всерьёз. Хрестоматийный предостерегающий пример у Месароша — Ariane 5: оставленный активным в полёте наземный код спровоцировал катастрофу. Оставленный включённым if (isTesting) — программная версия того же самого.
  • Безопасность. Тестовые обходы — это бэкдоры: флаг, пропускающий аутентификацию, оплату или валидацию, отделён от эксплуатируемой уязвимости всего одной ошибкой конфигурации.
  • Читаемость и раздувание API. Тестовые сеттеры/геттеры/reset() расширяют публичную поверхность и вводят реальных клиентов в заблуждение относительно назначения класса.
  • Сопровождаемость. В одном классе живут два поведения; при каждом изменении приходится держать в голове и продакшен-путь, и тестовый, а их расхождение тихо загнивает.

##Treatment

Вынесите тестовую логику из продакшен-кода, введя нормальный шов вместо флага.

  1. Внедрите вариативность (внедрение зависимости + тестовый дублёр). Замените жёстко зашитую ветвь коллаборатором, которого тест подменяет. В продакшене подключается настоящая реализация; в тесте — фейк/стаб/мок.
  2. Используйте тестовый подкласс (Test-Specific Subclass), когда нужно переопределить лишь один метод — переопределите его в подклассе, который живёт в тестовом коде, а не через if в базовом классе.
  3. Примените паттерн Humble Object, чтобы вынести трудно тестируемую логику (часы, ввод-вывод) за тонкий адаптер, и тогда основная логика станет напрямую тестируемой без хуков.
  4. Перенесите логику сравнения на сторону теста. Вместо того чтобы загрязнять продакшен методом equals() ради проверок, используйте собственный матчер/компаратор или проверяйте только интересующие вас поля.
  5. Не пускайте тестовый код в сборку. Используйте разделение по source-set / конфигурации сборки, чтобы тестовые помощники нельзя было скомпилировать в поставляемый артефакт; помечайте действительно видимые для тестов члены аннотациями @VisibleForTesting/@TestOnly и поручите линтеру следить, чтобы продакшен их никогда не вызывал.
// ДО: тестовый хук внутри продакшен-кода
class PaymentService {
  charge(order: Order) {
    if (this.isTesting) return { status: 'ok', id: 'FAKE-TEST-ID' };
    return this.gateway.charge(order);
  }
}

// ПОСЛЕ: единственный путь; шлюз внедряется и подменяется в тесте
class PaymentService {
  constructor(private gateway: PaymentGateway) {}
  charge(order: Order) {
    return this.gateway.charge(order); // один и тот же путь в продакшене и тесте
  }
}

// тест
const fakeGateway = { charge: () => ({ status: 'ok', id: 'FAKE-TEST-ID' }) };
const service = new PaymentService(fakeGateway);

Теперь продакшен-путь — это единственный путь, а тест управляет поведением снаружи.

##Detected by

  • codeql java/visible-for-testing-abuseИспользование VisibleForTesting в продакшен-коде
  • deepsource JAVA-A1067Методы @VisibleForTesting/@TestOnly не должны использоваться в нетестовом коде
  • android-lint VisibleForTestsВидимый только для тестов