Суть Читай реальный тестовый код — первый падающий тест, хрупкий mock-тяжёлый тест, property и выжившего мутанта — и выбирай фикс или диагноз, который senior делает первым.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Качество тестов оценивается в файле теста, а не в отчёте о покрытии. Читай каждый сниппет как PR коллеги, затем выбери изменение, которое senior сделал бы первым.
Цель
Натренируй взгляд, с которым приходишь на реальное ревью: замечать тест, что проверяет структуру вместо поведения, писать падающий тест, который ведёт дизайн, формулировать property, что бьёт примеры, и читать, о чём говорит выживший мутант.
Сниппет 1 — первый падающий тест
// Ведём ещё не написанное правило возврата, тест-сначала.test("refund for a fully-shipped order is rejected", () => { const order = anOrder({ status: "SHIPPED" }); const result = refundPolicy.evaluate(order); expect(result.allowed).toBe(false); expect(result.reason).toBe("ALREADY_SHIPPED");});
Викторина
Completed
До того как refundPolicy существует, что даёт написание этого теста первым и почему он хорошо сформирован?
Heads-up Шаг red — несущий. Вызов evaluate(order) до его существования форсирует решения по интерфейсу — имя, аргументы, форму возврата, режим отказа — в самый дешёвый момент, до того как появятся зависящие места вызова.
Heads-up Это привязывает тест к внутренней структуре вызовов — антипаттерн хрупкого теста. Он сломается на безобидном рефакторинге и может остаться зелёным, пока исход возврата неверен. Проверяй исход, а не танец.
Heads-up Один проходящий пример доказывает один случай, а не отсутствие багов. Продукт шага red — обратная связь по дизайну; широкий поиск багов добавят property и mutation testing позже.
Сниппет 2 — хрупкий тест
test("checkout charges the customer", () => { const tax = mock(TaxRule); // чистый, наш собственный const gateway = mock(PaymentGateway); // сторонняя сеть when(tax.apply(100)).thenReturn(108); checkout.pay(cart, tax, gateway); verify(tax.apply(100)).once(); // проверяем, что вызов был verify(gateway.charge(108)).once();});
Викторина
Completed
Сохраняющий поведение рефакторинг checkout делает этот тест красным, но реальный баг вычисления суммы однажды уехал с ним зелёным. В чём корневая причина?
Heads-up Матчеры в порядке; дефект — в дизайне теста. Проверять взаимодействия на собственном чистом коде — это ловушка over-mocking независимо от того, как написаны матчеры.
Heads-up Конкурентность тут ни при чём. Тест падает на рефакторинге, потому что проверяет, какие вызовы случились, и пропускает баги, потому что застабил результат, который должен был вычислить.
Heads-up Покрытие тут неважно — строка прогналась. Баг проскочил, потому что проверяемая сумма была застабленной константой (108), а не реальным вычислением. Проверяй вычисленное состояние с реальным правилом.
Heads-up Round-trip проверяет parse∘serialize против тождества и может пройти, пока оба ошибаются одинаково. Oracle сравнивает с независимым доверенным эталоном, поэтому ловит расхождения, которые round-trip не может.
Heads-up Воспроизводимость даёт напечатанный seed, а не число прогонов. Перезапусти с seed — и повторишь точный падающий случай; numRuns лишь задаёт, насколько жёстко атакует генератор.
Heads-up fc.string() генерирует произвольные строки, включая кавычки, запятые и переводы строк — именно те ужасы с запятой-в-кавычках и встроенным переводом строки, что ломают реальные CSV-парсеры. Эта широта и есть смысл.
Сниппет 4 — выживший мутант
// Продакшн-код под mutation testing:function withinBudget(amount, limit) { return amount <= limit; // Stryker мутировал в: amount < limit}// Полный набор всё равно прошёл после мутации. Мутант ВЫЖИЛ.
Викторина
Completed
Мутант '<=' → '<' выжил при 100% покрытия строк. О чём это говорит и какой фикс?
Heads-up Они различаются ровно при amount === limit. У эквивалентного мутанта нет входа, который его отличает; у этого есть, так что это реальный пробел, а не шум для отбрасывания.
Heads-up Предпосылка — 100% покрытия строк, строка прогонялась каждый раз. Он выжил, потому что ни один assertion не различил граничный случай, что покрытие как раз и не может измерить.
Heads-up Покрытие уже максимально и всё равно это пропустило. Фикс — конкретный assertion при amount === limit; гонка за числом покрытия, что уже 100%, ничего не меняет.
Итог
Каждый тест в этом юните читается одинаково. Первый падающий тест — это обратная связь по дизайну: он фиксирует интерфейс и проверяет наблюдаемый исход, поэтому переживает рефакторинги. Хрупкий тест over-mock’ает собственный чистый код: падает на рефакторинге и отгружает баги зелёным за застабленными результатами — используй реальные объекты и проверяй состояние. Oracle-property даёт доверенному эталону задавать корректность над сгенерированным входом и шринкует до минимального контрпримера. А выживший мутант — это именованный недостающий assertion, чаще всего граница, которую набор никогда не подал. Диагностируй по тесту, чини assertion, перезапускай для подтверждения, что мутант убит.