Невнятный тест.
Невнятный тест — это тест, в котором читатель по одному только методу теста не может понять, какой сценарий настроен, какое поведение проверяется и какой результат ожидается, потому что намерение погребено под избытком деталей, недостатком контекста или логикой, спрятанной где-то ещё.
##Признаки и симптомы
Вы читаете тест и всё равно не можете ответить на три вопроса: какова настройка, что именно проверяется и каков ожидаемый результат? Цепочка причина/следствие скрыта. Месарос группирует причины на «слишком много информации» и «слишком мало информации». Типичные приметы:
- Eager Test (жадный тест) — один метод теста проверяет много несвязанных вариантов поведения, поэтому никакое единое намерение не ясно.
- Mystery Guest (таинственный гость) — фикстура или ожидаемые значения живут вне теста (файл, общая запись в БД, фикстура, загружаемая по имени), поэтому вы не видите, почему проверка должна пройти.
- General Fixture (общая фикстура) — большой общий
beforeEach/фабрика строит куда больше, чем нужно этому тесту; немногие значимые поля тонут в шуме. - Irrelevant Information (несущественная информация) — страницы данных настройки или гигантский снимок, где на деле важны лишь одно-два поля. Подробно — в статье «Многословный тест».
- Hard-Coded Test Data (захардкоженные тестовые данные) — «магические» литералы (
42,"a3f9-...",userId=7) без именованного смысла, повторяющиеся в настройке и в проверках. См. «Тест с магическими числами». - Indirect Testing (косвенное тестирование) — тест тыкает в проверяемую систему через несколько других объектов, затемняя то, что на самом деле проверяется.
// Невнятно: Eager + Mystery Guest + Irrelevant Information
test('user', async () => {
const data = loadFixture('users.json'); // таинственный гость: значения живут в файле
const svc = new UserService(data, cfg, clock); // общая фикстура: большая часть не используется
const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
expect(u.id).toBeDefined();
expect(u.email).toContain('@');
expect(await svc.count()).toBe(data.length + 1); // почему это число? ответ — в файле
expect(await svc.login(u.email, 'p@ss')).toBe(true); // несвязанное второе поведение
});
Здесь невозможно понять, что доказывает тест, не открыв users.json, и он на деле проверяет создание и вход за один раз.
##Причины проблемы
Почему это происходит
- Тесты разрастаются наслоением: разработчик добавляет «ещё одну проверку» в существующий тест вместо того, чтобы написать новый, порождая Eager Test (жадный тест).
- Совместное использование настройки кажется эффективным, поэтому единая широкая фикстура или
beforeEachпереиспользуется повсюду (General Fixture, общая фикстура), а внешние файлы/наполнение БД загружаются по имени (Mystery Guest, таинственный гость). - Копирование правдоподобно выглядящих данных тащит за собой десятки несущественных полей и «магических чисел».
- Чрезмерное использование моков или проход через множество сотрудников-зависимостей превращает модульный тест в Indirect Testing (косвенное тестирование).
Почему это вредно
- Читаемость: тест — это документация задуманного поведения. Если читатель не может восстановить цепочку «Настройка → Действие → Проверка», эта документационная ценность теряется.
- Сопровождаемость: когда невнятный тест падает, вы не знаете, какое поведение сломалось и ошибка в тесте или в коде, поэтому изменения медленны и рискованны.
- Надёжность: Mystery Guest и General Fixture связывают тест с внешним/общим состоянием, вызывая нестабильные или зависящие от порядка падения (и нарушая свойство «свежий, детерминированный» хорошего модульного теста).
- Ложная уверенность: Eager Test маскирует покрытие — падение в начале метода обрывает выполнение последующих проверок, поэтому поведение, которое вы думаете, что протестировано, может никогда не выполниться. И, как отмечает Месарос, ошибки в коде проще спрятать в невнятном тесте, порождая Buggy Tests (баговые тесты), которые проходят по неверным причинам.
##Лечение
Единого лекарства от невнятного теста нет: «невнятность» — симптом, у которого несколько причин. Сначала поставьте диагноз, затем примените средство против найденной причины — у каждой есть своя статья с подробными рефакторингами.
- Устройте «холодное чтение». Попросите человека, не видевшего код, по одному лишь телу теста назвать подготовку, действие и ожидаемый результат. Там, где ему пришлось открыть другой файл или гадать, и находится причина.
- Несколько несвязанных вариантов поведения в одном тесте? Это нетерпеливый тест: разбейте его на сфокусированные тесты, каждый из которых назван по проверяемому поведению. Заодно раннее падение перестанет скрывать последующие проверки.
- Ожидаемые значения обосновываются внешним файлом, сидом или общей записью? Это таинственный гость: перенесите в тест данные, от которых зависит проверка.
- Широкий общий
beforeEachстроит куда больше, чем нужно тесту? Это универсальная фикстура: перейдите на минимальную свежую фикстуру для каждого теста. - Страницы несущественной подготовки или поштучные проверки полей? Это многословный тест: спрячьте шум за методами создания и билдерами и сравнивайте с ожидаемым объектом — механика подробно разобрана там.
- Необъяснённые литералы? См. «Тест с магическими числами»: дайте значениям имена, чтобы проверка объясняла сама себя.
- Проверяемая система доступна только через цепочку других объектов (косвенное тестирование)? Тестируйте её через её собственный интерфейс или вынесите логику так, чтобы её можно было проверить напрямую.
// До (невнятно)
test('user', async () => {
const data = loadFixture('users.json');
const svc = new UserService(data, cfg, clock);
const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
expect(await svc.count()).toBe(data.length + 1);
});
// После: одно намерение, фикстура встроена, данные именованы
test('create() persists a new user', async () => {
const svc = userServiceWith([]); // минимальная, свежая фикстура
const newUser = aUser({ email: '[email protected]' }); // строитель задаёт значения шума по умолчанию
const created = await svc.create(newUser);
expect(created.email).toBe('[email protected]');
expect(await svc.count()).toBe(1); // самоочевидно, без внешнего файла
});
Стремитесь к форме AAA (Arrange-Act-Assert, Подготовка-Действие-Проверка): читатель должен ухватить сценарий, не покидая метода теста.
##Обнаружение анализаторами
- eslint-jest jest/max-expects — Ограничивает число вызовов expect() в одном тесте
- eslint-vitest vitest/max-expects — Ограничивает число вызовов expect() в одном тесте
- eslint-jest jest/no-large-snapshots — Запрещает большие снапшоты
- eslint-vitest vitest/no-large-snapshots — Запрещает большие снапшоты
- eslint-jest jest/max-nested-describe — Ограничивает глубину вложенности блоков describe()