Нетерпеливый тест.
Нетерпеливый тест проверяет несколько разных методов или поведений тестируемого модуля в одном тестовом методе вместо того, чтобы сосредоточиться на одном поведении.
##Signs and Symptoms
Один тестовый метод прогоняет много несвязанных боевых методов и проверяет каждый, проводя объект через целую последовательность операций вместо проверки одного результата.
Характерные признаки:
- Расплывчатое, всеохватное имя теста (
testUserService,it('works')) или сшитое из «и» (creates_and_renames_and_deletes). - Несколько циклов действие → проверка в одном теле: вы вызываете метод, проверяете, вызываете другой, проверяете снова.
- Комментарии, используемые как заголовки секций внутри теста (
// now test delete), чтобы разделить проверяемые вещи. - Проверки затрагивают несколько разных методов/полей SUT, не имеющих общего логического результата.
// ЗАПАХ: один тест проверяет create, rename, delete и list
test('user service', () => {
const svc = new UserService();
const user = svc.create({ name: 'Ada' }); // поведение 1
expect(user.id).toBeDefined();
svc.rename(user.id, 'Grace'); // поведение 2
expect(svc.get(user.id).name).toBe('Grace');
svc.delete(user.id); // поведение 3
expect(svc.get(user.id)).toBeUndefined();
expect(svc.list()).toHaveLength(0); // поведение 4
});
Обратите внимание на различие: нетерпеливый тест — это о проверке нескольких поведений, а не просто о наличии нескольких expect. Несколько проверок одного логического результата (например, проверка нескольких полей одного и того же возвращённого объекта) — это нормально и не является этим запахом.
##Reasons for the Problem
Почему это происходит
- Переиспользование подготовки / лень. Подготовка фикстуры утомительна или дорога, поэтому соблазнительно продолжать тыкать в уже построенный объект и проверять «раз уж мы здесь».
- Мышление сценариями. Автор тестирует пользовательский сценарий («создать, затем отредактировать, затем удалить») как один скрипт вместо изоляции каждого отдельного поведения модуля.
- Дрейф TDD. Тест, начавшийся сфокусированным, накапливает лишние проверки по мере того, как новая функциональность прикручивается к нему, а не получает свой собственный тест.
Почему это вредно
- Ложная уверенность (главное). Большинство библиотек проверок прерывают тест на первой упавшей проверке. Если
createсломан, проверкиrename,deleteиlistникогда не выполнятся — поэтому переход из зелёного в красный скрывает, сколько поведений на самом деле сломано, а проходивший тест на самом деле никогда не прогонял поздние шаги, как только регрессировал один из ранних. - Плохая диагностика. Падение говорит вам «тест сервиса пользователей упал», а не какое поведение. Чтобы найти сломанный шаг, приходится читать весь метод.
- Затуманенное намерение / читаемость. Тест больше не документирует один факт о системе; читателю приходится мысленно разбивать его на поведения, которые он связывает в пучок.
- Хрупкость и связанность. Поздние проверки зависят от ранних мутаций, поэтому несвязанное изменение в начале каскадирует и делает тест ломким и трудным для рефакторинга.
- Сопровождаемость. Труднее удалить, переместить или переименовать покрытие поведения, когда оно переплетено с тремя другими в одном методе.
##Treatment
Разбейте нетерпеливый тест на несколько сфокусированных тестов — одно поведение на тест — и поднимите общий шаг подготовки в setup, чтобы разбиение не дублировало шаблонный код.
Шаги:
- Перечислите поведения, которые тест связывает в пучок (здесь: создание, переименование, удаление, список после удаления).
- Извлеките по одному тесту на поведение, каждый с описательным, раскрывающим намерение именем.
- Перенесите общую подготовку в
beforeEachили фабрику/метод создания, чтобы каждый тест по-прежнему имел чистую SUT без скопированного кода подготовки. - Оставьте по одному действию (Act) на тест и проверяйте только результат этого поведения (несколько
expectпо одному результату — это нормально). - Если вам действительно нужно проверить сквозной сценарий, оставьте его как один явно названный сценарный/интеграционный тест — но всё равно покрывайте каждое отдельное поведение собственным тестом, а не полагайтесь на сценарий ради покрытия.
- Защититесь от регрессий, включив правило максимума проверок (см. детекторы) как дешёвый суррогат.
// ПОСЛЕ: сфокусированные тесты, общая подготовка
let svc;
beforeEach(() => { svc = new UserService(); });
test('create() assigns an id', () => {
expect(svc.create({ name: 'Ada' }).id).toBeDefined();
});
test('rename() updates the stored name', () => {
const { id } = svc.create({ name: 'Ada' });
svc.rename(id, 'Grace');
expect(svc.get(id).name).toBe('Grace');
});
test('delete() removes the user', () => {
const { id } = svc.create({ name: 'Ada' });
svc.delete(id);
expect(svc.get(id)).toBeUndefined();
});
Теперь любое отдельное поведение может упасть независимо, имя упавшего теста точно указывает на поломку, и каждый тест читается как один задокументированный факт о SUT.
##Detected by
- eslint-jest jest/max-expects — Ограничить максимальное число вызовов expect() на тест
- eslint-vitest vitest/max-expects — Ограничить максимальное число expect на тест