Невнятный тест.
Невнятный тест — это тест, в котором читатель по одному только методу теста не может понять, какой сценарий настроен, какое поведение проверяется и какой результат ожидается, потому что намерение погребено под избытком деталей, недостатком контекста или логикой, спрятанной где-то ещё.
##Signs and Symptoms
Вы читаете тест и всё равно не можете ответить на три вопроса: какова настройка, что именно проверяется и каков ожидаемый результат? Цепочка причина/следствие скрыта. Месарос группирует причины на «слишком много информации» и «слишком мало информации». Типичные приметы:
- 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, и он на деле проверяет создание и вход за один раз.
##Reasons for the Problem
Почему это происходит
- Тесты разрастаются наслоением: разработчик добавляет «ещё одну проверку» в существующий тест вместо того, чтобы написать новый, порождая Eager Test (жадный тест).
- Совместное использование настройки кажется эффективным, поэтому единая широкая фикстура или
beforeEachпереиспользуется повсюду (General Fixture, общая фикстура), а внешние файлы/наполнение БД загружаются по имени (Mystery Guest, таинственный гость). - Копирование правдоподобно выглядящих данных тащит за собой десятки несущественных полей и «магических чисел».
- Чрезмерное использование моков или проход через множество сотрудников-зависимостей превращает модульный тест в Indirect Testing (косвенное тестирование).
Почему это вредно
- Читаемость: тест — это документация задуманного поведения. Если читатель не может восстановить цепочку «Настройка → Действие → Проверка», эта документационная ценность теряется.
- Сопровождаемость: когда невнятный тест падает, вы не знаете, какое поведение сломалось и ошибка в тесте или в коде, поэтому изменения медленны и рискованны.
- Надёжность: Mystery Guest и General Fixture связывают тест с внешним/общим состоянием, вызывая нестабильные или зависящие от порядка падения (и нарушая свойство «свежий, детерминированный» хорошего модульного теста).
- Ложная уверенность: Eager Test маскирует покрытие — падение в начале метода обрывает выполнение последующих проверок, поэтому поведение, которое вы думаете, что протестировано, может никогда не выполниться. И, как отмечает Месарос, ошибки в коде проще спрятать в невнятном тесте, порождая Buggy Tests (баговые тесты), которые проходят по неверным причинам.
##Treatment
Сделайте так, чтобы каждый тест рассказывал самодостаточную историю, намерение которой видно в теле теста.
- Проверяйте одно условие на тест. Разбейте Eager Test на сфокусированные тесты, каждый названный по поведению, которое он проверяет. Это также устраняет маскировку покрытия.
- Встройте нужную фикстуру (уберите Mystery Guest). Постройте данные, от которых зависит тест, в самом тесте или через явный, именованный метод создания/строитель — не загружайте анонимные файлы или общие строки БД.
- Используйте минимальную / свежую фикстуру. Стройте только то, что нужно этому тесту; замените широкий общий
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: 'ada@example.com' }); // строитель задаёт значения шума по умолчанию
const created = await svc.create(newUser);
expect(created.email).toBe('ada@example.com');
expect(await svc.count()).toBe(1); // самоочевидно, без внешнего файла
});
Стремитесь к форме AAA (Arrange-Act-Assert, Подготовка-Действие-Проверка): читатель должен ухватить сценарий, не покидая метода теста.
##Detected by
- eslint-plugin-jest max-expects — jest/max-expects
- eslint-plugin-vitest max-expects — vitest/max-expects
- eslint-plugin-jest no-large-snapshots — jest/no-large-snapshots
- eslint-plugin-vitest no-large-snapshots — vitest/no-large-snapshots
- eslint-plugin-jest max-nested-describe — jest/max-nested-describe