---
title: "Тестовая логика в продакшен-коде"
type: "test-smell"
slug: "test-logic-in-production"
url: "http://localhost:3000/ru/test-smells/test-logic-in-production.md"
category: "Запахи связанности"
description: "Продакшен-код содержит логику, ветви или члены, существующие лишь для поддержки тестирования, что размывает грань между тем, что попадает в продакшен, и тем, что просто тестируется."
---
# Тестовая логика в продакшен-коде

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

## Signs and Symptoms

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

* **Тестовые хуки / флаги режима** — ветви, завязанные на флаг `testing`, `isTest`, `NODE_ENV === 'test'` или `mock`, которые замыкают накоротко настоящее поведение.
* **Члены «только для тестов»** — публичные сеттеры, геттеры, `reset()` или конструкторы, добавленные исключительно для того, чтобы тест мог добраться до внутреннего состояния, нередко помеченные `@VisibleForTesting` / `@TestOnly`, а затем реально вызываемые из продакшена.
* **Загрязнение сравнением** — логика `equals()` / сравнения, добавленная в продакшен-класс лишь для того, чтобы проверка могла сравнить два объекта.
* **Зависимость от тестов в продакшене** — продакшен-модули, импортирующие тестовый фреймворк, фикстуру или фабрику моков.

```ts
// ЗАПАХ: поставляемый код ведёт себя иначе, когда включён «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` и поручите линтеру следить, чтобы продакшен их никогда не вызывал.

```ts
// ДО: тестовый хук внутри продакшен-кода
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 в продакшен-коде (https://codeql.github.com/codeql-query-help/java/java-visible-for-testing-abuse/)
- **deepsource** `JAVA-A1067` — Методы @VisibleForTesting/@TestOnly не должны использоваться в нетестовом коде (https://deepsource.com/directory/java/issues/JAVA-A1067)
- **android-lint** `VisibleForTests` — Видимый только для тестов (https://googlesamples.github.io/android-custom-lint-rules/checks/VisibleForTests.md.html)
