Раздел 29 · Тестирование

Ошибки в тестах: галерея антипаттернов и чеклист для ревью

middle~40 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Ошибки в тестах: галерея антипаттернов и чеклист для ревью

Сцена · галерея антипаттернов

Зелёный набор тестов это ещё не хороший набор. На ревью ты раз за разом встречаешь одни и те же болезни: тест, который проверяет, как код написан, а не что он делает; один гигантский объект, от которого зависит полнабора; ассерт, повторяющий формулу из продакшна; десять моков вокруг одной функции. Каждая из них пройдёт CI и будет светиться зелёным, пока однажды не подведёт.

Эта серия уже разбирала эти ошибки поодиночке: поведение против реализации, эвристики качества, овермок. Здесь мы собираем их в одну галерею с примерами до и после и в готовый чеклист из десяти пунктов, по которому проходишь любой тест перед мерджем, свой и агентский.

Карта урока · что заберёшь

  • узнаешь шесть главных антипаттернов теста в лицо, каждый с примером до и после;
  • поймёшь, чем опасен рожок мороженого и как его выправить;
  • получишь чеклист из десяти пунктов для ревью теста;
  • научишься применять чеклист к коду агента, который воспроизводит антипаттерны массово.

Раздел 1 · шесть антипаттернов

Идея словами

У каждого антипаттерна одна и та же беда: тест перестаёт ловить баги, но продолжает выглядеть полезным. Разберём шесть самых частых, на минимальных примерах.

Антипаттерн 1 · тест на реализацию

Тест дёргает приватный метод или проверяет, как именно посчитан результат, а не сам результат. Любой рефактор красит его зря.

// до: привязка к тому, КАК считается
expect(account._internalReducer).toHaveBeenCalledWith('SUBTRACT', 30);

// после: проверяем результат, а не внутренний путь
const next = applyPayment(account, makePayment({ amount: 30 }));
expect(next.balance).toBe(70);

Антипаттерн 2 · тавтология

Ассерт повторяет формулу из продакшна. Если в коде ошибка в формуле, она же приедет в тест, и оба согласятся.

// до: дублирует реализацию, поймает только опечатку
expect(applyPayment(acc, p).balance).toBe(acc.balance - p.amount);

// после: ожидаемое число посчитано человеком, а не формулой
expect(applyPayment(makeAccount({ balance: 100 }), makePayment({ amount: 30 })).balance).toBe(70);

Антипаттерн 3 · вечнозелёный тест

Тест физически не может покраснеть. Чаще всего из-за забытого await или ассерта, который всегда истинен.

// до: промис не дождались, тест зелёный даже при reject
it('отвергает перерасход', () => {
  expect(charge(acc, 1000)).rejects.toThrow(); // нет await/return
});

// после: ошибку дождались и проверили как значение
it('отвергает перерасход', async () => {
  const result = await Effect.runPromise(charge(acc, 1000).pipe(Effect.result));
  expect(result).toBeFailure(PaymentRejected);
});

Вторая, менее очевидная форма того же антипаттерна: if в теле теста. Чаще всего он появляется не по злому умыслу, а ради сужения типа.

// до: настоящие проверки спрятаны в ветку, которая может не выполниться
expect(Result.isFailure(result)).toBe(true);
if (Result.isFailure(result)) {
  expect(result.failure._tag).toBe('PaymentRejected');
}

// после: сужение унесено в матчер, ветки нет
expect(result).toBeFailure(PaymentRejected);

Пока первая строка на месте, всё выглядит рабочим. Но ослабь её или потеряй при мердже, и тело if перестанет исполняться: тест не проверяет ничего и при этом зелёный. Правило простое: в тесте не должно быть условий. Если условие понадобилось ради типов, его место в матчере (29-testing/06 · Раннер и ассерты), а не в тесте.

Антипаттерн 4 · god-фикстура

Один огромный объект на все тесты. Меняешь одно поле, краснеет половина набора, которая на это поле даже не смотрела (урок 07). Лечится factory с overrides: в тесте видно только важное поле.

Антипаттерн 5 · овермок

Замокано всё вокруг функции, тест проверяет, что код вызвал то, что вызвал, и краснеет от любого рефактора (урок 13). Лечится in-memory fake (в Effect это тестовый Layer).

Антипаттерн 6 · тест без причины падать

Несколько ассертов в одном тесте проверяют разное, и неясно, что именно сломалось при падении. Один тест, один повод падать (AAA).

Когда код пишет агент

Агент воспроизводит эти антипаттерны массово, потому что они “проходят”: тавтология всегда зелёная, овермок всегда зелёный, вечнозелёный по определению. Сгенерировал двадцать тестов, и среди них пяток тавтологий и пара вечнозелёных. Чеклист из следующего раздела это твой фильтр на ревью, а mutation testing автоматический детектор того же.

Что взять с собой

  • антипаттерн опасен тем, что выглядит полезным, оставаясь зелёным;
  • тест на реализацию, тавтология, вечнозелёный, god-фикстура, овермок, много поводов падать это шесть лиц одной болезни;
  • у каждого есть лечение, разобранное в предыдущих уроках.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.