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

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

## Signs and Symptoms

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

Типичные признаки:

* **Проверки несущественных деталей.** Точные пробелы/разметка, полные строки сообщений об ошибках, текст логов, форматирование или изменчивые значения вроде временных меток, UUID и автоинкрементных идентификаторов.
* **Сравнение объекта целиком там, где важно одно поле.** `toEqual({...весь объект...})`, когда тест на самом деле об одном-единственном свойстве.
* **Чувствительные к порядку проверки на логически неупорядоченных данных.** Фиксация порядка множества, ключей отображения (map) или результатов запроса, который контракт не гарантирует.
* **Проверка поведения внутренних сотрудников-зависимостей.** Моки, проверяющие точное число вызовов, значения аргумент за аргументом и порядок вызовов _внутренних_ зависимостей вместо наблюдаемого конечного результата.
* **Гигантские снимки (snapshots)**, захватывающие всё отрендеренное дерево или тело ответа, так что любое вложенное изменение вынуждает перезаписывать снимок.
* **Влезание в реализацию.** Обращение к приватному состоянию или структуре DOM (`container.querySelector`, внутренние поля) вместо публичного, наблюдаемого пользователем поведения.

```js
// Переспецифицировано: фиксирует точную разметку, изменчивую временную метку и каждый вызов сотрудника
test('renders user badge', () => {
  const html = renderBadge(user);
  expect(html).toBe('<span class="badge badge--admin">Ada Lovelace</span>'); // точная разметка
  expect(analytics.track).toHaveBeenCalledTimes(1);                          // внутренний счётчик вызовов
  expect(analytics.track).toHaveBeenCalledWith('badge_render', { ts: 1718445693221 }); // изменчивое
});

```

Полезная лакмусовая проверка: если можно изменить реализацию, не меняя задокументированное поведение, а тест всё равно падает — он переспецифицирован.

## Reasons for the Problem

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

* Установка «больше проверок = тщательнее»: разработчики фиксируют всё, что видят, путая _полноту_ с _корректностью_.
* Инструментарий делает переспецификацию путём наименьшего сопротивления: `toMatchSnapshot()` записывает весь вывод целиком, а фреймворки моков делают тривиальной проверку каждого взаимодействия (`toHaveBeenCalledWith`, порядок вызовов, их количество).
* Копирование реального литерала объекта в `toEqual` вместо проверки только того поля, о котором этот тест.
* Тестирование через удобные зацепки реализации (DOM-узлы, приватные поля) вместо публичного контракта.

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

В книге Джерарда Месароса _xUnit Test Patterns_ это коренная причина запаха **Fragile Test** (хрупкий тест), приписываемая **Overspecified Software** и **Behavior Sensitivity**: тесты связаны с тем, _как_ работает код, а не с тем, _что_ он производит, поэтому они падают при изменениях, не влияющих на поведение.

* **Сопровождаемость.** Каждый рефакторинг вызывает каскад несвязанных падений тестов, из-за чего держать набор «зелёным» дорого, и это активно отбивает желание рефакторить.
* **Надёжность / доверие.** Тесты, которые «кричат „волки!“» на корректном коде, приучают команду игнорировать падения или вслепую перезаписывать их (особенно со снимками-снапшотами).
* **Ложная уверенность.** Переспецификация часто соседствует с недостаточной проверкой _значимого_ результата: тест может зафиксировать временную метку и строку лога, но так и не проверить значение, которое на самом деле важно пользователю. Как формулирует литература по тестированию взаимодействий, переспецификация — это «проверка того, что не является частью конечного результата», чаще всего через проверку взаимодействий вместо результатов.
* **Читаемость.** Стена побочных проверок скрывает то единственное поведение, которое тест призван документировать.

## Treatment

Проверяйте только то, что требует проверяемое поведение, — и предпочитайте проверку **конечного результата** внутренним взаимодействиям.

1. **Назовите тот единственный результат, который документирует тест**, и проверяйте именно его и ничего больше. По-настоящему различные результаты разнесите по отдельным, хорошо названным тестам, а не в одну мега-проверку.
2. **Используйте частичные/нестрогие матчеры** вместо точного сравнения объекта целиком: `expect.objectContaining`, `toMatchObject`, `arrayContaining`, `stringContaining`/`stringMatching` и `expect.any(...)` для полей, которые нельзя или не следует фиксировать.
3. **Нейтрализуйте изменчивые данные** (временные метки, идентификаторы, случайные значения) матчерами свойств (`expect.any(String)`) или путём внедрения фиксированных часов/генератора идентификаторов — не «зашивайте» сегодняшнее значение.
4. **Откажитесь от предположений о порядке** для неупорядоченных данных: проверяйте принадлежность (`arrayContaining`) или сортируйте перед сравнением.
5. **Предпочитайте проверку состояния проверке поведения.** Подменяйте заглушками _запросы_ (не проверяйте их); проверяйте вызов сотрудника-зависимости лишь тогда, когда этот вызов _и есть_ наблюдаемый побочный эффект (команда), и избегайте проверки точного числа/порядка чисто внутренних вызовов.
6. **Держите снимки маленькими и осмысленными** или замените раскидистый снимок несколькими прицельными проверками. Обращайтесь через API, обращённый к пользователю (например, `getByRole`/`getByText` из Testing Library), вместо доступа к `container`/DOM-узлам.

До → после:

```js
// До: привязывает тест к изменчивым полям и последовательности внутренних вызовов
expect(result).toEqual({
  id: '8f3c-92a1-...',                  // случайный uuid
  createdAt: '2026-06-15T10:01:33.221Z',// Date.now()
  name: 'Ada Lovelace',
  role: 'admin',
});
expect(logger.info).toHaveBeenCalledTimes(3);
expect(db.connect).toHaveBeenCalledBefore(db.query);

// После: проверяем только то поведение, о котором этот тест
expect(result).toMatchObject({ name: 'Ada Lovelace', role: 'admin' });
// id/createdAt побочны; логирование и порядок вызовов — детали реализации, не фиксируйте их

```

## Detected by

- **eslint-jest** `jest/no-large-snapshots` — no-large-snapshots (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/no-large-snapshots.md)
- **eslint-vitest** `vitest/no-large-snapshots` — no-large-snapshots (https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/no-large-snapshots.md)
- **eslint-testing-library** `testing-library/no-node-access` — no-node-access (https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-node-access.md)
- **eslint-testing-library** `testing-library/no-container` — no-container (https://github.com/testing-library/eslint-plugin-testing-library/blob/main/docs/rules/no-container.md)
