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

> Тест упаковывает множество недокументированных проверок в один метод, так что при падении нельзя понять, какая проверка сработала и почему — приходится гадать.

## Signs and Symptoms

Один тест содержит череду голых проверок без поясняющих сообщений и без ясной единственной цели. Когда он краснеет, отчёт о падении (особенно строка-сводка CI или общий помощник для проверок) сообщает, _что_ что-то сломалось, но не _какая_ проверка и _почему_ — вы играете в «рулетку», чтобы найти виновника.

Выдающие признаки:

* Много вызовов `expect(...)` / `assert(...)` в одном тесте, ни один из которых не несёт описательного сообщения или метки.
* Имя теста обобщённое (`'works'`, `'user profile'`, `'test register'`), так что падение само по себе ничего не сообщает.
* Проверки охватывают несколько несвязанных аспектов ([Eager Test](https://en.wikipedia.org/wiki/Test%5Fsmell), переиспользующий одну фикстуру, чтобы проверить всё разом).
* Падение вынуждает вас лезть в отладчик или к номерам строк, чтобы понять, что вообще проверялось.
* Поскольку большинство проверок отказывают быстро, первый провал обрывает остальные, поэтому вы никогда не увидите последующие проверки за один прогон.

```js
test('user profile', () => {
  const user = createUser({ name: 'Ada', age: 36 });
  expect(user.name).toBe('Ada');
  expect(user.age).toBe(36);
  expect(user.isAdult).toBe(true);   // если красной окажется ИМЕННО эта,
  expect(user.slug).toBe('ada');     // отчёт просто скажет
  expect(user.roles).toContain('member'); // «expected false to be true»
  expect(user.createdAt).toBeInstanceOf(Date);
});

```

Замечание: современные раннеры (Jest, Vitest) печатают упавшую строку и diff, что смягчает проблему «какая строка?». Запах всё равно кусается, когда тест смешивает цели, имеет расплывчатое имя, перебирает проверки в цикле, прячет их за общими помощниками или выполняется там, где выживает лишь сводное сообщение.

## Reasons for the Problem

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

* _Eager Test / переиспользование фикстуры._ Дорогая настройка соблазняет навалить проверок «раз уж я здесь» вместо написания второго теста.
* _Проверка объекта целиком, сделанная сложным путём._ Проверка объекта поле за полем естественно порождает длинную череду недокументированных проверок.
* _Рост копипастом._ Тесты со временем обрастают проверками, и никто их не разделяет.
* _Исторический инструментарий._ Классические проверки xUnit сообщали лишь успех/провал, поэтому проверка без метки не давала контекста при падении — отсюда и исходный запах у Месароса.
* _Давление сроков._ Один большой тест кажется быстрее в написании, чем несколько сфокусированных.

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

* _Диагностируемость / надёжность._ По данным testsmells.org, «множественные операторы проверки в методе теста без описательного сообщения ухудшают читаемость/понятность/сопровождаемость, так как невозможно понять причину падения». Вы тратите время на поиск того, какая проверка сработала.
* _Скрытые дефекты (ложная уверенность)._ Проверки с быстрым отказом останавливаются на первом провале, поэтому последующие проверки никогда не выполняются. Вы чините одну, перезапускаете, находите следующую — несколько багов замаскированы, и «зелёный после одной правки» ощущается безопаснее, чем есть.
* _Читаемость._ Тест перестаёт документировать единственное поведение; его намерение погребено в списке, а обобщённое имя теста ничего не добавляет.
* _Сопровождаемость._ Неясно, относится ли новая проверка к этому тесту или к новому, поэтому куча продолжает расти, а несвязанные аспекты оказываются сцеплены.

## Treatment

Стремитесь к принципу, стоящему за тестом с одним условием (Single-Condition Test): **каждый тест проверяет одно поведение и может упасть ровно по одной причине.**

1. **Разделяйте по цели.** Разбейте универсальный тест на сфокусированные тесты с описательными именами. Тогда имя _и есть_ сообщение о падении.
2. **Сворачивайте проверки поле за полем в одну проверку.** Используйте `toEqual` / `expect.objectContaining` / просмотренный снимок, чтобы N проверок стали одним осмысленным diff.
3. **Когда группировка по-настоящему связна, помечайте проверки.** У `expect` в Jest/Vitest нет аргумента-сообщения, поэтому используйте параметр-сообщение из `node:assert`, `expect(actual, message)` в Vitest или `jest-expect-message`.
4. **Хотите, чтобы все проверки выполнялись и отчитывались вместе?** Предпочтите разделение; иначе используйте мягкие проверки (`expect.soft` в Vitest), чтобы один провал не скрывал остальные.
5. **Защититесь от регрессии**, ограничив число проверок на тест (см. детекторы).

До — рулетка проверок:

```js
test('register user', () => {
  const res = register({ email: 'a@b.com', age: 36 });
  expect(res.ok).toBe(true);
  expect(res.user.email).toBe('a@b.com');
  expect(res.user.isAdult).toBe(true);
  expect(res.welcomeEmailSent).toBe(true);
});

```

После — одно поведение на тест, с проверкой объекта целиком:

```js
describe('register', () => {
  it('accepts a valid adult signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).ok).toBe(true);
  });

  it('stores the normalized user record', () => {
    const { user } = register({ email: 'a@b.com', age: 36 });
    expect(user).toEqual(
      expect.objectContaining({ email: 'a@b.com', isAdult: true }),
    );
  });

  it('sends a welcome email on signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).welcomeEmailSent).toBe(true);
  });
});

```

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

```js
import { strict as assert } from 'node:assert';
assert.equal(res.ok, true, 'registration should succeed');
assert.equal(res.welcomeEmailSent, true, 'welcome email should be sent');

```

## Detected by

- **eslint-jest** `jest/max-expects` — Отмечает основанный на подсчёте суррогат Assertion Roulette: сообщает, когда тест превышает N вызовов expect() (по умолчанию 5). Документация отмечает, что большее число проверок склонно смешивать несколько целей. (https://github.com/jest-community/eslint-plugin-jest/blob/main/docs/rules/max-expects.md)
- **eslint-vitest** `vitest/max-expects` — Аналог для Vitest: устанавливает максимальное число проверок expect() на тест, отлавливая тесты, в которые упихано слишком много проверок. (https://github.com/vitest-dev/eslint-plugin-vitest/blob/main/docs/rules/max-expects.md)
- **sonar** `java:S5961` — SonarSource «Test methods should not contain too many assertions» — ограничивает число проверок на тест (по умолчанию 25 для JUnit/AssertJ), стандартный суррогат этого запаха для статического анализа. (https://rules.sonarsource.com/java/RSPEC-5961/)
