ConstructiCat Logo
CodeBust.
Browse section ▾

Условная логика в тесте.

Тест, который использует `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

Сделайте каждый тест единственным, безусловным, линейным путём. Конкретно:

  1. Один сценарий на тест. Разбейте ветвящийся тест на отдельные тесты или используйте управляемый данными API фреймворка (test.each, it.each, параметризованные тесты), чтобы каждый случай был собственным, ясно названным, отдельно отчитывающимся прогоном.
  2. Вынесите ветвление за пределы теста. Если случай применим лишь при некотором условии, решайте это на этапе определения (например, describe/it, выбираемые по конфигурации), а не в теле теста — сами проверки остаются безусловными.
  3. Захардкодьте ожидаемые значения. Замените вычисляемые ожидания литеральными ожидаемыми результатами (или Expected Object / пользовательским матчером). Не воспроизводите продакшен-логику в тесте.
  4. Тестируйте пути ошибок матчерами, а не try/catch: expect(fn).toThrow(...), await expect(p).rejects.toThrow(...). Они громко падают, когда ошибка не выброшена.
  5. Если условная проверка по-настоящему неизбежна, зафиксируйте их число через expect.assertions(n) / expect.hasAssertions(), чтобы пропущенная ветка приводила к падению, а не к тихому прохождению.
  6. Замените условную очистку хуками жизненного цикла фреймворка (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