Рулетка проверок.
Тест упаковывает множество недокументированных проверок в один метод, так что при падении нельзя понять, какая проверка сработала и почему — приходится гадать.
##Signs and Symptoms
Один тест содержит череду голых проверок без поясняющих сообщений и без ясной единственной цели. Когда он краснеет, отчёт о падении (особенно строка-сводка CI или общий помощник для проверок) сообщает, что что-то сломалось, но не какая проверка и почему — вы играете в «рулетку», чтобы найти виновника.
Выдающие признаки:
- Много вызовов
expect(...)/assert(...)в одном тесте, ни один из которых не несёт описательного сообщения или метки. - Имя теста обобщённое (
'works','user profile','test register'), так что падение само по себе ничего не сообщает. - Проверки охватывают несколько несвязанных аспектов (Eager Test, переиспользующий одну фикстуру, чтобы проверить всё разом).
- Падение вынуждает вас лезть в отладчик или к номерам строк, чтобы понять, что вообще проверялось.
- Поскольку большинство проверок отказывают быстро, первый провал обрывает остальные, поэтому вы никогда не увидите последующие проверки за один прогон.
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): каждый тест проверяет одно поведение и может упасть ровно по одной причине.
- Разделяйте по цели. Разбейте универсальный тест на сфокусированные тесты с описательными именами. Тогда имя и есть сообщение о падении.
- Сворачивайте проверки поле за полем в одну проверку. Используйте
toEqual/expect.objectContaining/ просмотренный снимок, чтобы N проверок стали одним осмысленным diff. - Когда группировка по-настоящему связна, помечайте проверки. У
expectв Jest/Vitest нет аргумента-сообщения, поэтому используйте параметр-сообщение изnode:assert,expect(actual, message)в Vitest илиjest-expect-message. - Хотите, чтобы все проверки выполнялись и отчитывались вместе? Предпочтите разделение; иначе используйте мягкие проверки (
expect.softв Vitest), чтобы один провал не скрывал остальные. - Защититесь от регрессии, ограничив число проверок на тест (см. детекторы).
До — рулетка проверок:
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);
});
После — одно поведение на тест, с проверкой объекта целиком:
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);
});
});
Если приходится оставить один тест, сделайте каждую проверку самоописывающей:
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). Документация отмечает, что большее число проверок склонно смешивать несколько целей.
- eslint-vitest vitest/max-expects — Аналог для Vitest: устанавливает максимальное число проверок expect() на тест, отлавливая тесты, в которые упихано слишком много проверок.
- sonar java:S5961 — SonarSource «Test methods should not contain too many assertions» — ограничивает число проверок на тест (по умолчанию 25 для JUnit/AssertJ), стандартный суррогат этого запаха для статического анализа.