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

> Боевой код содержит методы, аксессоры состояния или швы, которые существуют только для использования тестами, загрязняя реальный API и провоцируя тесты, проверяющие внутренности вместо поведения.

## Signs and Symptoms

Вы находите члены в **боевом** коде, чьи единственные вызывающие живут в тестовых файлах. Характерные признаки:

* Методы, геттеры/сеттеры, экспорты или параметры конструктора, используемые исключительно из `*.test.ts` / `*.spec.ts`.
* Имена или маркеры с тестовым «привкусом»: `getStateForTest`, `resetForTesting`, `__getInternal`, `FTO_*`, `forTest`, комментарии вроде `// only used by tests` или аннотации наподобие `@VisibleForTesting` / `@TestOnly` / `@internal`.
* Ослабленная видимость (поле сделано `public`/экспортируемым, `#private` превращено в `protected`) исключительно ради того, чтобы тест мог добраться до внутреннего состояния.
* Лишние «швы» — `setClock(...)`, `setRandom(...)` или `reset()` — добавленные только потому, что они понадобились тесту, а не потому, что этого требует реальный дизайн.

```ts
// payment-service.ts  (БОЕВОЙ код)
export class PaymentService {
  #ledger: Entry[] = [];

  charge(amount: number) { /* ... */ }

  // В продакшене это никто не вызывает — только тесты:
  getLedgerForTest() { return this.#ledger; }          // раскрывает внутренности
  setClockForTest(now: () => Date) { this.now = now; } // шов только для тестов
  FTO_reset() { this.#ledger = []; }                   // "For Tests Only"
}

```

Быстрая проверка: `grep -rn 'forTest\|ForTesting\|FTO_' src/`, дающий совпадения в боевых исходниках, или прогон поиска мёртвого кода (например, Knip в продакшен-режиме), который сообщает, что экспорт не используется, хотя тест его явно импортирует.

## Reasons for the Problem

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

* _Дооснащение тестами непроверяемого кода._ Когда легаси-код не проектировался под тестируемость, самый быстрый способ проверить результат — пробить дыру в SUT и прочитать его внутренности; Месарош называет это главной причиной _For Tests Only_.
* _Асимметричные API._ Реальные клиенты используют объект в одну сторону (запись); тесты используют его симметрично (запись, **а затем** чтение для проверки), поэтому тестировщикам «нужны» аксессоры, которые не нужны ни одному боевому вызывающему.
* _Давление сроков._ Добавить чёрный ход здесь и сейчас дешевле, чем рефакторить к дизайну, где поведение наблюдаемо через публичный контракт.

**Почему это вредно**

* _Читаемость._ Публичный API больше не говорит правду — сопровождающие не могут отличить реальный контракт от тестовых лесов, и каждый читатель вынужден гадать «а используется ли этот метод вообще?»
* _Инкапсуляция и надёжность._ Внутреннее состояние становится достижимым и изменяемым в поставляемом коде. Случайный вызов `FTO_reset()` или просочившийся сеттер может повредить состояние в продакшене; лишняя поверхность также раздувает бандл и расширяет поверхность атаки.
* _Ложная уверенность._ Тесты, которые ковыряются в приватном состоянии, проверяют _реализацию_, а не _поведение_. Они могут оставаться зелёными, пока публичный контракт сломан, и ломаться при безобидных рефакторингах — хрупкие тесты, проверяющие не то.
* _Сопровождаемость._ Члены «только для тестов» выглядят как мёртвый код, но их нельзя удалить; каждое изменение должно учитывать фантомных вызывающих, и запах склонен размножаться по мере того, как всё больше тестов переиспользуют этот чёрный ход.

## Treatment

Относитесь к чёрному ходу как к сигналу о дизайне, а не как к детали фикстуры.

1. **Сначала тестируйте через наблюдаемое поведение.** Проверяйте возвращаемые значения, испускаемые события, сохранённый вывод или взаимодействия с коллабораторами (через тестовые дублёры) вместо того, чтобы лезть во внутренности. Большинство аксессоров `getXForTest` исчезают, как только вы проверяете, что объект _делает_, а не что он _хранит_.
2. **Используйте тест-специфичный подкласс (Test-Specific Subclass)**, когда внутренний доступ действительно нужен. Расширьте класс в тесте, чтобы раскрыть `protected`\-член, вместо расширения видимости в боевом коде.
3. **Делайте швы частью реального дизайна, а не люками только для тестов.** Внедрение часов или ГСЧ через обычный конструктор — это законное внедрение зависимостей; мутатор `setClockForTest()` — это запах. Если шов имеет смысл только для тестов, протолкните поведение в **стратегию/Null-объект**, который продакшен устанавливает по умолчанию, а тест подменяет.
4. **Если раскрытия действительно не избежать, пометьте его громко и огородите.** Отметьте его как `@VisibleForTesting` / `@internal` (или используйте соглашение об именовании `FTO_`) и добейтесь того, чтобы боевой код никогда его не вызывал (см. детекторы). Помеченный, охраняемый шов лучше молчаливого.
5. **Выследите существующих нарушителей.** Сделайте `grep` по `forTest`/`ForTesting`/`FTO_` и запустите инструмент анализа использования/мёртвого кода, например Knip в продакшен-режиме, чтобы выявить экспорты, на которые ссылаются только тестовые файлы.

```ts
// ДО — боевой код несёт аксессор только для тестов
export class Cart {
  #items: Item[] = [];
  add(i: Item) { this.#items.push(i); }
  getItemsForTest() { return this.#items; } // только для тестировщиков
}
// тест
expect(cart.getItemsForTest()).toHaveLength(1);

```

```ts
// ПОСЛЕ — проверяем поведение через реальный контракт
export class Cart {
  #items: Item[] = [];
  add(i: Item) { this.#items.push(i); }
  get count() { return this.#items.length; }
  get total() { return this.#items.reduce((s, i) => s + i.price, 0); }
}
// тест
cart.add({ price: 10 });
expect(cart.count).toBe(1);
expect(cart.total).toBe(10);

```

Если внутренний доступ всё же нужен, сузьте шов до тестов с помощью подкласса, а не открывайте его всему миру:

```ts
// продакшен остаётся чистым: queue имеет модификатор protected, а не public
export class Scheduler {
  protected queue: Job[] = [];
  enqueue(j: Job) { this.queue.push(j); }
}
// только в тестовом файле
class TestScheduler extends Scheduler {
  peek() { return this.queue; }
}

```

## Detected by

- **sonar** `java:S5803` — «@VisibleForTesting»-члены не должны вызываться из боевого кода (https://rules.sonarsource.com/java/RSPEC-5803/)
