Что делает модульный тест хорошим — Примеры

Пример 1 — Тест, который ломается при каждом рефакторинге

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

test("тест рассчитать цену 2", () => {
  const calculator = new PriceCalculator();
  const result = calculator.calculate(memberCart);
  expect(calculator.rounding).toHaveBeenCalledWith(1000);
  expect(calculator.discounts).toHaveBeenCalledTimes(1);
  expect(result).toBe(900);
});

Решение

test("применяет десять процентов участнику", () => {
  const cart = cartWith(item("book", 1000), member);

  const total = new PriceCalculator().calculate(cart);

  expect(total).toBe(900);
});

Исходный тест утверждает, что были вызваны два помощника, — а это описание текущей реализации: выделите вспомогательную функцию, встроьте округление или переставьте применение скидки, и он провалится, хотя цена всё ещё верна. Утверждение о возвращённой сумме проверяет решение, которое принимает модуль, поэтому проходит любой рефакторинг, сохраняющий ответ 900. Ловушка в том, что утверждения о вызовах легко добавить, когда помощник уже стал шпионом, и они ощущаются как дополнительная надёжность — до первого рефакторинга, который превращает их в причину отказаться от изменения.

Пример 2 — Пять утверждений и одно сообщение о провале

Тест ниже проверяет всё правило срока действия сразу, а сообщается в нём только о первом проваленном утверждении, и имя не описывает ни одного поведения. Скажите, что читатель узнаёт из красного прогона, затем перепишите это в тесты, каждый из которых проваливается по одной причине.

test("срок действия", () => {
  const card = { expires: "2024-01" };
  expect(isValid(card, today("2024-02-01"))).toBe(false);
  expect(isValid(card, today("2024-01-15"))).toBe(true);
  expect(isValid(null, today("2024-01-15"))).toBe(false);
});

Решение

test("отвергает карту, когда её месяц прошёл", () => {
  expect(isValid({ expires: "2024-01" }, today("2024-02-01"))).toBe(false);
});

test("принимает карту в её последний месяц", () => {
  expect(isValid({ expires: "2024-01" }, today("2024-01-15"))).toBe(true);
});

test("отвергает отсутствующую карту", () => {
  expect(isValid(null, today("2024-01-15"))).toBe(false);
});

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