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

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

## Signs and Symptoms

Этот запах живёт в _продакшен_\-коде, но обнаруживаете вы его во время написания тестов. Характерная примета — модульный тест не получается написать чисто: тесту приходится бороться с дизайном, чтобы привести тестируемый код в известное состояние и наблюдать результат.

Высматривайте такие симптомы:

* **Гигантские блоки arrange.** Чтобы просто инстанцировать тестируемый класс, приходится конструировать паутину коллабораторов («Я не могу протестировать `OrderService`, не подняв заодно БД, шлюз и загрузчик конфигурации»).
* **Нет шва для внедрения дублёра.** Код вызывает `new ConcreteThing()`, обращается к статическому синглтону (`Database.getInstance()`) или напрямую лезет в сеть/файловую систему/часы, поэтому подставить заглушку или фейк негде.
* **Недетерминизм, вшитый внутрь.** Логика зависит от `Date.now()`, `Math.random()`, `process.env` или системного времени, которые тест не может контролировать, что даёт нестабильные или невоспроизводимые результаты.
* **Нет точки наблюдения / точки управления.** Результат, который вы хотите проверить, погребён в приватном состоянии или побочном эффекте, поэтому тесты тянутся к нему через рефлексию, приведения типов или геттеры только для тестов.
* **Сугубо асинхронный интерфейс.** Единственный способ привести код в движение — запустить таймер/поток/очередь, а затем `sleep()` или опрашивать, потому что завершение никогда не наблюдаемо напрямую.
* **Вы меняете продакшен-код только ради того, чтобы его протестировать.** Ослабляете `private` до `public`, добавляете тестовые хуки `if (testMode) { ... }` или наследуетесь лишь для того, чтобы добраться до внутренностей.

```js
// Трудно тестировать: скрытые + жёстко зашитые зависимости, глобальное состояние, недетерминизм
class OrderService {
  placeOrder(cart) {
    const db = Database.getInstance();            // глобальный синглтон (нет шва)
    const gateway = new StripeGateway(API_KEY);   // жёстко зашитая конкретная зависимость -> реальная сеть
    const id = Math.random().toString(36).slice(2); // недетерминированно
    const charge = gateway.charge(cart.total);    // реальный HTTP-вызов внутри модуля
    db.save({ id, at: Date.now(), charge });       // не внедрённые часы
    return id;
  }
}
// Чтобы «протестировать это юнитом», вам придётся ходить в Stripe, глобально подменять Date/Math
// и инспектировать общий синглтон — то есть Indirect Testing через неуклюжие интерфейсы.

```

## Reasons for the Problem

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

* **Test-Last на легаси-коде.** Мезарос называет коренной причиной _отсутствие проектирования под тестируемость_. Тестируемость естественно вытекает из TDD, но когда тесты пишутся «в последнюю очередь», ничто не вынуждало дизайн открывать точки управления и наблюдения, поэтому их приходится встраивать задним числом.
* **Тесная связанность и жёстко зашитые зависимости.** Создание конкретных коллабораторов через `new` (или извлечение их из статических синглтонов) означает, что вы не можете заменить их тестовыми дублёрами — тест прогоняет и сам модуль, _и_ всё, к чему он прикасается.
* **Глобальное/разделяемое состояние и скрытые входы.** Синглтоны, статика, окружающее время, случайность и чтение окружения — это входы, которые тест никогда не объявлял и не может задать, поэтому поведение неявно и неуправляемо.
* **Асинхронность и побочные эффекты в ядре.** Когда бизнес-логика переплетена с потоками, очередями, вводом-выводом или UI, не остаётся чистого значения, которое можно было бы проверить.

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

* **Надёжность.** Тесты, вынужденные использовать реальный ввод-вывод, sleep-ы, общие синглтоны или глобальные часы, становятся медленными, нестабильными и зависимыми от порядка — классический путь к Erratic/Fragile Test.
* **Сопровождаемость.** Огромная подготовка и тесты, лезущие во внутренности, привязывают набор тестов к деталям реализации, поэтому безобидные рефакторинги ломают тесты (Fragile Test / Fragile Fixture).
* **Ложная уверенность.** Самые трудные и важные пути кода тестируются только через грубые косвенные интерфейсы — или пропускаются вовсе. Цифры покрытия выглядят прилично, а рискованная логика едва прогоняется.
* **Читаемость.** Тест, в котором доминируют леса, скрывает то единственное поведение, которое он должен специфицировать, поэтому он ничего не документирует.

В Open Catalog of Test Smells _Hard-To-Test Code_ — это парный со стороны продакшена аналог тестовых запахов вроде _Indirect Testing_ и _Fragile Test_: плохой дизайн — причина, неуклюжие тесты — симптом.

## Treatment

Чините _дизайн_, а не тест. Цель — дать каждому модулю **точку управления** (способ привести его в известное состояние) и **точку наблюдения** (способ считать результат).

1. **Введите швы через внедрение зависимостей.** Передавайте коллабораторов извне (внедрение через конструктор) вместо того, чтобы создавать или искать их. Внедряйте и окружающие входы — часы, генератор id/uuid, источник случайности, — чтобы они стали управляемыми параметрами.
2. **Зависьте от абстракций.** Программируйте на интерфейс и подставляйте в тестах заглушку/фейк/мок. Это напрямую устраняет _Hard-wired Dependency_ от Designite и сокращает _Excessive Dependency_.
3. **Примените паттерн Humble Object.** Вытолкните по-настоящему трудно тестируемые части (UI, асинхронную обвязку, сырой ввод-вывод) в тонкий адаптер без логики, а логику принятия решений перенесите в простой синхронный объект, который можно тестировать напрямую.
4. **Извлеките чистые функции.** Отделите вычисления от побочных эффектов; проверяйте возвращаемые значения, а ввод-вывод выполняйте только на границе.
5. **Сделайте асинхронность наблюдаемой.** Возвращайте промис/future или выставляйте сигнал завершения, а также внедряйте планировщик, чтобы тесты использовали фейковые таймеры вместо `sleep`.
6. **Для легаси, который пока нельзя перепроектировать,** используйте _Test-Specific Subclass_ или узко ограниченный шов (приёмы Физерса), чтобы взять код под тест, а затем рефакторите в сторону внедрения — вместо того чтобы оставлять в продакшене постоянные хуки `if (testMode)`.
7. **Избегайте глобального/статического состояния.** Замените синглтоны явно передаваемыми зависимостями, чтобы тесты не делили и не сбрасывали скрытое состояние.

```js
// ПОСЛЕ: зависимости, часы и id внедряются -> тестируемо в изоляции
class OrderService {
  constructor(db, gateway, clock = () => Date.now(), newId = uuid) {
    this.db = db; this.gateway = gateway; this.clock = clock; this.newId = newId;
  }
  placeOrder(cart) {
    const id = this.newId();
    const charge = this.gateway.charge(cart.total); // абстракция PaymentGateway
    this.db.save({ id, at: this.clock(), charge });
    return id;
  }
}

// Тест: детерминированный, без сети, без глобальных подмен, быстрый.
const svc = new OrderService(fakeDb, fakeGateway, () => 1000, () => 'id-1');
expect(svc.placeOrder({ total: 50 })).toBe('id-1');
expect(fakeGateway.charge).toHaveBeenCalledWith(50);
expect(fakeDb.saved[0]).toEqual({ id: 'id-1', at: 1000, charge: fakeGateway.result });

```

## Detected by

- **designite** `Hard-wired Dependency (testability smell)` — Hard-wired Dependency (https://www.designite-tools.com/blog/understanding-testability-test-smells)
- **designite** `Excessive Dependency (testability smell)` — Excessive Dependency (https://www.designite-tools.com/blog/understanding-testability-test-smells)
- **designite** `Global State (testability smell)` — Global State (https://www.designite-tools.com/blog/understanding-testability-test-smells)
- **designite** `Law of Demeter Violation (testability smell)` — Law of Demeter Violation (https://www.designite-tools.com/blog/understanding-testability-test-smells)
