Условная логика в тесте.
Тест, который использует `if`/`switch`/тернарные операторы, циклы или `try`/`catch`, чтобы решать, что выполнять или проверять, так что его поведение — и проверяет ли он что-либо вообще — зависит от того, какая ветка выполнится во время работы.
##Signs and Symptoms
Тест читается как небольшая программа, а не как линейный сценарий «подготовка → действие → проверка». Ищите поток управления в теле теста:
if/else,switch, тернарные операторы или короткозамкнутые&&/||, которые управляют тем, какие проверки выполняются.- Циклы
for/while/forEach, которые строят входные данные или перебирают проверки. try/catch, используемый для «тестирования» пути ошибки, с вызовамиexpect, спрятанными внутриcatch.- Один тест, переиспользуемый для нескольких случаев через ветвление по флагу или состоянию среды.
- Ожидаемые значения, вычисляемые в тесте (нередко циклом или формулой), вместо захардкоженных.
// Запах: проверки находятся внутри условного/ветвящегося кода
test('user discount', () => {
const user = getUser();
if (user.isPremium) {
expect(price(user)).toBe(80); // может никогда не выполниться
} else {
expect(price(user)).toBe(100); // может никогда не выполниться
}
});
test('throws on bad input', () => {
try {
parse('!!!');
// если parse() НЕ выбрасывает, мы проваливаемся вниз и ничего не проверяем → тест проходит
} catch (err) {
expect(err.message).toMatch(/invalid/);
}
});
Характерный режим отказа: тест остаётся зелёным даже когда код сломан, потому что ветка, содержащая проверку, ни разу не была выбрана.
##Reasons for the Problem
Почему это происходит
- DRY, доведённый до крайности. Авторы пытаются покрыть несколько сценариев одним «гибким» тестом, ветвясь по входным данным или флагу, вместо того чтобы писать по тесту на случай (Месарос: Flexible Test).
- Связанность со средой. Проверяемая система (SUT) не была развязана от своих зависимостей, поэтому тест подстраивается под то состояние, которое находит во время выполнения.
- Защитная очистка.
if (resource) resource.close()закрадывается, чтобы не разрушать фикстуры, которых может не быть (Complex Teardown). - Вычисляемые ожидания. Ожидаемый результат выводится тем же алгоритмом, что и продакшен-код, затаскивая эту логику — со всеми циклами — в тест (Production Logic in Test).
- Тестирование пути ошибки вручную.
try/catchиспользуется для проверки выброшенной ошибки вместо встроенного матчера.
Почему это вредно
- Ложная уверенность (худшее). Большинство раннеров проваливают тест, только когда проверка выбрасывает исключение. Если проверяющая ветка пропущена — или проверяемый код не выбрасывает исключение внутри
try, — тест проходит, не проверив ничего. - Непротестированный код теста. Ветвления и циклы в тесте — это логика, у которой самой нет тестов; баг в собственном потоке управления теста остаётся незамеченным.
- Плохая диагностика. Когда ветвящийся тест падает, сначала нужно выяснить, какой путь выполнился, прежде чем толковать падение.
- Сниженная читаемость и сопровождаемость. Линейный тест документирует одно поведение с одним ожидаемым результатом; ветвящийся тест вынуждает читателя моделировать выполнение, чтобы понять, что на самом деле гарантировано.
- Хрупкость. Тесты, завязанные на состояние времени выполнения/среды, проходят или падают недетерминированно.
##Treatment
Сделайте каждый тест единственным, безусловным, линейным путём. Конкретно:
- Один сценарий на тест. Разбейте ветвящийся тест на отдельные тесты или используйте управляемый данными API фреймворка (
test.each,it.each, параметризованные тесты), чтобы каждый случай был собственным, ясно названным, отдельно отчитывающимся прогоном. - Вынесите ветвление за пределы теста. Если случай применим лишь при некотором условии, решайте это на этапе определения (например,
describe/it, выбираемые по конфигурации), а не в теле теста — сами проверки остаются безусловными. - Захардкодьте ожидаемые значения. Замените вычисляемые ожидания литеральными ожидаемыми результатами (или Expected Object / пользовательским матчером). Не воспроизводите продакшен-логику в тесте.
- Тестируйте пути ошибок матчерами, а не
try/catch:expect(fn).toThrow(...),await expect(p).rejects.toThrow(...). Они громко падают, когда ошибка не выброшена. - Если условная проверка по-настоящему неизбежна, зафиксируйте их число через
expect.assertions(n)/expect.hasAssertions(), чтобы пропущенная ветка приводила к падению, а не к тихому прохождению. - Замените условную очистку хуками жизненного цикла фреймворка (
afterEach) и автоматической/идемпотентной очисткой, чтобы не требовалсяifдля защиты очистки.
// До — ветвящийся тест, проверки могут быть пропущены
test('user discount', () => {
const user = getUser();
if (user.isPremium) expect(price(user)).toBe(80);
else expect(price(user)).toBe(100);
});
// После — один явный случай на строку, каждая проверка всегда выполняется
test.each([
['premium', { isPremium: true }, 80],
['regular', { isPremium: false }, 100],
])('price for %s user', (_label, user, expected) => {
expect(price(user)).toBe(expected);
});
// До — try/catch, проходящий, когда ничего не выброшено
try { parse('!!!'); } catch (e) { expect(e.message).toMatch(/invalid/); }
// После — падает, если parse() не выбрасывает
expect(() => parse('!!!')).toThrow(/invalid/);
##Detected by
- eslint-jest jest/no-conditional-expect — no-conditional-expect
- eslint-jest jest/no-conditional-in-test — no-conditional-in-test
- eslint-vitest vitest/no-conditional-expect — no-conditional-expect
- eslint-vitest vitest/no-conditional-in-test — no-conditional-in-test
- eslint-vitest vitest/no-conditional-tests — no-conditional-tests