# Тесты — это как менять код без страха

Экспресс-курс о тестировании. 19 понятий — Unit Test, Integration Test, E2E, Test Pyramid, Stub и Mock, TDD, Code Coverage, Flaky Test, CI Gate и другие — у каждого английское название, схема, карточка и пример. Главная мысль: смысл тестов не в одноразовом доказательстве правильности, а в свободе менять код завтра, не ломая того, что работает сегодня. В конце — шпаргалка по всем терминам.

- **Author:** Фёдор Чищин
- **Level:** intro
- **Reading time:** ~35 минут чтения
- **Published:** 2026-06-13
- **Source:** https://fedorthinks.com/ru/learn/testing-express

---

## 1. Зачем мы тестируем

Почти все неверно понимают, для чего тесты. Они выглядят как способ доказать, что код работает. Их настоящая ценность глубже и практичнее — и когда ты это видишь, ты пишешь тесты по-другому.

### Карта: 19 понятий в семи главах

> Карта метро: сначала видишь все линии целиком, потом едешь по станциям.

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

_Diagram: Семь глав и 19 понятий курса. В конце — шпаргалка по всем._

### Как читать курс

> Паспорт: одинаковые поля у всех, поэтому их легко сравнивать.

У каждого понятия дальше одно и то же: **английское название** (запомни его — это термин, который ты встретишь в документации и на собеседованиях), образ из жизни, **схема** и **карточка** с полями: _проблема_, _идея_, _пример_, _цена_, _когда брать_ и _связи_ с другими понятиями. Главная мысль всего курса одна: **тесты — это страховка для будущих изменений**, а не справка о том, что код работал однажды.

### Automated Test — тест как код, который проверяет код

> Второй человек, который пересчитывает твои суммы на калькуляторе и отмечает каждую, что не сходится, — автоматически, без устали и одинаково каждый раз.

**Тест (test)** — это маленький кусок кода, который запускает твой код с известным вводом и проверяет, что результат тот, что ты ждёшь. «Если дать `add` числа 2 и 3, вернёт ли она 5?» Если да, тест **проходит (pass)**; если нет — **падает (fail)** и сообщает тебе. Вот и вся механика. Сила в том, что это автоматически и повторяемо: тысячи таких проверок бегут за секунды при каждом изменении, и никому не надо перепроверять руками.

_Diagram: Тест даёт коду известный ввод и сравнивает результат с ожиданием: совпало — pass, нет — fail._

#### Automated Test (автотест)

- **Проблема:** Проверять руками долго и скучно — поэтому после каждой правки никто не перепроверяет всё.
- **Идея:** Записать проверку кодом: известный ввод → вызов → сравнение с ожиданием. Итог — pass или fail.
- **Пример:** «`add(2, 3)` должна вернуть 5» — одна строка, бежит за миллисекунды.
- **Цена:** Тесты — тоже код: их надо писать, читать и поддерживать.
- **Когда брать:** Для любого кода, который ты собираешься менять дальше.
- **Связи:** Ловит **Regression**; бывает **Unit**, **Integration** и **E2E**; бежит в **CI Gate**.

### Safety Net — настоящий смысл: менять без страха

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

Вот главное недопонимание. Тесты в основном не про то, чтобы доказать, что сегодняшний код верен, — они про **завтра**. Когда ты меняешь код, рефакторишь его или добавляешь фичу, хороший **набор тестов (test suite)** сразу говорит, не сломал ли ты то, что раньше работало. Эта **страховочная сеть (safety net)** и даёт уверенно менять большую кодовую базу, а не бояться её трогать. Тесты — это способ двигаться быстро, чтобы за спиной ничего тихо не ломалось.

_Diagram: Любое изменение проходит через набор тестов: зелёный — отгружай спокойно, красный — чини сейчас, а не после жалобы пользователя._

### Regression — регрессия

> Ряд растяжек по всему дому: как только что-то тревожит комнату, на которую ты даже не смотрел, срабатывает сигнал. Тебе не надо обходить каждую комнату самому.

**Регрессия (regression)** — это когда изменение ломает то, что раньше работало, часто где-то далеко от того места, которое ты трогал. Без тестов регрессии прячутся, пока их не найдёт пользователь. С тестами в тот же миг, как твоё изменение ломает старое поведение, тест краснеет и показывает, где именно. Это будничная отдача: внёс правку, прогнал тесты — и сразу знаешь, не сломал ли что-то ещё. Набор тестов стережёт весь дом, пока ты работаешь в одной комнате.

_Diagram: Ты поменял корзину, а сломались счета — три модуля дальше. Тест на счета краснеет сразу._

#### Regression (регрессия)

- **Что это:** Изменение ломает поведение, которое раньше работало.
- **Подвох:** Ломается часто не там, где ты правил, а в далёкой части системы.
- **Пример:** Поменял расчёт корзины — и счета начали выходить с неверной суммой.
- **Как ловить:** Набор тестов на старое поведение, который гоняют на каждом изменении.
- **Совет:** Нашёл баг — добавь тест на него, чтобы он не вернулся.
- **Связи:** Ловится **Automated Test**; страхует автоматически через **CI Gate**.

> **Вопрос рамки:** «Я пишу этот тест, чтобы доказать, что код работает один раз, — или чтобы поймать день, когда кто-то что-то поменяет и сломает это, сам того не заметив?»

> Тест проверяет код автоматически. Его настоящая ценность — не одноразовое доказательство, а свобода менять завтра, не ломая тихо то, что работает сегодня.

## 2. Юнит-тесты

Фундамент тестирования — юнит-тест: быстрая, точная проверка одного маленького куска кода самого по себе. Большинство твоих тестов будут такими, и не зря.

### Unit Test — один маленький кусок в изоляции

> Проверить, что один кубик Lego правильной формы и размера, прежде чем строить из него, — а не собирать весь замок, чтобы потом узнать, что одна деталь была кривой.

**Юнит-тест (unit test)** проверяет один маленький **юнит (unit)** — обычно одну функцию — в **изоляции (isolation)** от остальной системы. Дал ввод, проверил вывод, больше никто не участвует. Кусок маленький и сам по себе, поэтому, когда тест падает, ты точно знаешь, что сломалось и где. Юнит-тесты — микроскоп тестирования: узкий, точный, наведённый на одну вещь за раз.

_Diagram: Юнит-тест: ввод → одна функция → проверка вывода. База, сеть и диск не участвуют._

#### Unit Test (юнит-тест, модульный тест)

- **Проблема:** Когда падает большая проверка, непонятно, какая из сотни частей виновата.
- **Идея:** Проверять по одной функции, отрезав её от базы, сети и всего остального.
- **Пример:** `calculateTax(100)` возвращает `20` — без базы, без HTTP, за миллисекунды.
- **Цена:** Не видит, как куски работают вместе, — для этого нужны другие тесты.
- **Когда брать:** Для большей части тестов: бизнес-логика, расчёты, правила, краевые случаи.
- **Связи:** Основание **Test Pyramid**; изоляцию дают **Test Doubles**; форма — **Arrange–Act–Assert**.

### Fast and many — быстро и много, вот вся идея

> Проверка орфографии, которая проходит весь документ за мгновение, — так быстро, что её гоняешь постоянно, а не раз в месяц, как медленное ревью.

Каждый юнит-тест крошечный и не трогает ничего внешнего, поэтому он бежит за **миллисекунды**. Значит, их можно иметь тысячи и гонять все на каждом изменении, постоянно. В этой скорости и суть: набор, который бежит за секунды, запускают всё время — и поломку ловят в момент, когда она случилась. Медленный набор пропускают, а пропущенный набор не защищает ничего. Быстро и много — вот что делает юнит-тесты рабочей лошадкой.

_Diagram: Чем ниже уровень, тем быстрее тест. Быстрый набор гоняют на каждой правке — поэтому он и ловит поломки._

### Arrange–Act–Assert — подготовь, сделай, проверь

> Простой опыт: подготовь условия, сделай одно действие, проверь результат. Ясная подготовка, один ход, одна проверка.

У хорошего юнит-теста три простых шага — паттерн **Arrange–Act–Assert (AAA)**: подготовь вводы, один раз вызови то, что тестируешь, потом проверь, что результат тот, что ты ждал. **Ассерт (assertion)** — это сама проверка: «ответ должен быть равен 5». Тесты в этой чистой форме легко читать, а их падение сразу понятно. Тест, который трудно прочесть, — это тест, которому никто не будет доверять и который никто не станет поддерживать.

_Diagram: Три шага теста: Arrange готовит корзину, Act считает итог, Assert сверяет его с ожиданием._

#### Arrange–Act–Assert (AAA, подготовь-сделай-проверь)

- **Проблема:** Тест-каша: подготовка, вызовы и проверки вперемешку — не понять, что он проверяет.
- **Идея:** Три блока по порядку: подготовка → одно действие → проверка (assertion).
- **Пример:** Arrange: корзина с двумя товарами. Act: `cart.total()`. Assert: итог равен 20.
- **Цена:** Почти никакой — нужна только дисциплина «одно действие на тест».
- **Когда брать:** Всегда, в любом тесте — юнит, интеграционном или сквозном.
- **Связи:** Делает тест читаемым — а значит, **Living Documentation**; основа **Unit Test**.

> **Вопрос юнита:** «Могу ли я проверить этот один кусок сам по себе, быстро, с ясным arrange-act-assert — чтобы, когда тест упадёт, я точно знал, что сломалось?»

> Юнит-тест проверяет один маленький кусок в изоляции за миллисекунды — поэтому таких тестов тысячи, и они бегут постоянно. Подготовь ввод, сделай одно действие, проверь результат.

## 3. Интеграция, e2e и пирамида

Юнит-тесты проверяют куски поодиночке, но софт — это куски, которые работают вместе. Это покрывают два более широких вида тестов, а у правильного баланса всех трёх есть форма, которую стоит знать.

### Integration Test — куски, работающие вместе

> Проверив каждый кубик Lego, ты скрепляешь несколько вместе — убедиться, что они правда соединяются. По отдельности кубики были в порядке, но подходят ли они друг к другу?

**Интеграционный тест (integration test)** проверяет, что несколько частей верно работают _вместе_: твой код и база данных, два сервиса, которые обмениваются данными. Каждый юнит может быть идеален сам по себе и всё равно не состыковаться: не тот формат, неверное допущение о другой стороне. Интеграционные тесты ловят именно эти **швы (seams)**. Они медленнее юнит-тестов, потому что в них больше настоящих движущихся частей, но проверяют стыки, которые юнит-тесты по замыслу пропускают.

_Diagram: Интеграционный тест проверяет шов: запрос к базе и ответ, JSON между сервисами. Баги прячутся именно в стыках._

#### Integration Test (интеграционный тест)

- **Проблема:** Каждая часть проходит свои юнит-тесты, а вместе они не работают.
- **Идея:** Запустить несколько настоящих частей вместе и проверить стык между ними.
- **Пример:** Репозиторий пишет пользователя в настоящую тестовую базу и читает его обратно.
- **Цена:** Медленнее юнит-тестов, нужна настройка окружения (база, сервисы).
- **Когда брать:** Для швов: код ↔ база, сервис ↔ сервис, твой код ↔ чужой API.
- **Связи:** Средний слой **Test Pyramid**; ловит то, что прячет **Over-mocking**.

### End-to-End Test — вся система глазами пользователя

> Генеральная репетиция всей пьесы от начала до конца на настоящей сцене — не проверка реплик или реквизита по отдельности, а весь спектакль, как его увидит зритель.

**Сквозной тест (end-to-end test, E2E)** гоняет всю систему так, как это делал бы настоящий пользователь: нажать кнопку, заполнить форму, проверить, что нужное случилось через весь стек. Это самый реалистичный тест и самое ценное подтверждение, что продукт правда работает. Но он же самый медленный и **хрупкий (fragile)**: должно работать много частей сразу, а мелкое изменение интерфейса может его сломать. Поэтому e2e берут для немногих критичных путей, а не для всего.

_Diagram: E2E-тест идёт от пользователя через интерфейс, API и базу — и проверяет итог: заказ создан?_

#### End-to-End Test (E2E, сквозной тест)

- **Проблема:** Все части проверены, но работает ли продукт целиком — неизвестно.
- **Идея:** Пройти путь пользователя через весь настоящий стек и проверить итог.
- **Пример:** Браузер-робот: войти, положить товар в корзину, оплатить, увидеть «заказ оформлен».
- **Цена:** Самый медленный и хрупкий; ломается от мелких правок интерфейса.
- **Когда брать:** Для нескольких критичных путей: вход, оплата, главный сценарий.
- **Связи:** Вершина **Test Pyramid**; если таких тестов слишком много — «рожок мороженого».

### Test Pyramid — пирамида тестов

> Пирамида стоит, потому что основание у неё широкое, а вершина узкая. Переверни её на острие — и она упадёт. Сама форма и даёт устойчивость.

**Пирамида тестов (test pyramid)** — правило большого пальца для баланса: **много** быстрых юнит-тестов в основании, **меньше** интеграционных в середине и **горстка** медленных сквозных на вершине. Такая форма дёшево даёт широкую и быструю проверку, а сверху — ровно столько реалистичных проверок, сколько нужно. Антипаттерн — куча медленных e2e и мало юнит-тестов — называют **«рожком мороженого» (ice-cream cone)**: он медленный, флакающий и мучительный. Целься в пирамиду, а не в рожок.

_Diagram: Слева пирамида: много юнит-тестов, меньше интеграционных, пара e2e. Справа рожок мороженого — та же пирамида вверх ногами._

#### Test Pyramid (пирамида тестов)

- **Проблема:** Тестов много, но набор медленный и нестабильный — его перестают гонять.
- **Идея:** Много дешёвых быстрых тестов внизу, мало дорогих медленных наверху.
- **Пример:** Тысячи юнит-тестов, сотни интеграционных, десяток e2e на главные сценарии.
- **Цена:** Нужно осознанно решать, на каком уровне проверять каждую вещь.
- **Антипаттерн:** **Ice-cream cone** — пирамида вверх ногами: много e2e, мало юнитов.
- **Связи:** Слои — **Unit**, **Integration**, **E2E**; лишние e2e плодят **Flaky Tests**.

> **Вопрос пирамиды:** «Есть ли у меня много быстрых юнит-тестов в основании, немного интеграционных для швов и лишь пара сквозных для критичных путей — или моя пирамида перевёрнута?»

> Юнит-тесты проверяют куски поодиночке, интеграционные — вместе, сквозные — всю систему глазами пользователя. Держи пирамиду: много быстрых, мало медленных.

## 4. Подделки: стабы и моки

Чтобы проверить кусок в изоляции, часто нужно подменить настоящие вещи, от которых он зависит. Это и есть моки и стабы — и ими так же легко навредить, как помочь.

### Test Double — подделка вместо настоящей зависимости

> Авиатренажёр вместо настоящего самолёта: поддельные приборы и вид из окна дают отработать один навык безопасно, без цены и риска настоящего полёта.

Чтобы проверить кусок в изоляции, его настоящие **зависимости (dependencies)** — базу, платёжный сервис, отправку писем — подменяют **подделками**. Общее название для любой такой подделки — **тестовый двойник (test double)**, как дублёр в кино. Так ты проверяешь свой код без медленной, дорогой или непредсказуемой настоящей вещи. Юнит-тест не должен на самом деле списывать деньги с карты или слать письмо — ты подставляешь подделку, которая делает вид, и тест остаётся быстрым, надёжным и самодостаточным.

_Diagram: Код под тестом говорит с подделками, а не с настоящими Postgres, Stripe и почтой — те медленные, платные или шлют всё по-настоящему._

#### Test Double (тестовый двойник, подделка, fake)

- **Проблема:** Настоящие зависимости медленные, платные или непредсказуемые — тест с ними хрупкий.
- **Идея:** Подставить вместо зависимости объект, который ведёт себя как она, но понарошку.
- **Пример:** Вместо Stripe — подделка, которая всегда отвечает «оплата прошла».
- **Цена:** Подделка может врать: настоящая вещь ведёт себя иначе.
- **Когда брать:** Для внешнего, медленного и случайного: сеть, платежи, почта, время.
- **Связи:** Два главных вида — **Stub** и **Mock**; даёт изоляцию **Unit Test**; в избытке — **Over-mocking**.

### Stub vs Mock — заготовленный ответ против проверки вызова

> Стаб — картонная фигура, которая по сигналу говорит одну реплику. Мок — актёр, который ещё и докладывает, что именно ты ему сказал и как.

Два вида подделок часто путают. **Стаб (stub)** просто возвращает фиксированный, заготовленный ответ — «пусть база вернёт вот этого пользователя», — чтобы проверить, как твой код обработает такой ввод. **Мок (mock)** ещё и _проверяет взаимодействие_ — «убедись, что мой код вызвал `sendEmail` ровно один раз и с этим адресом». Стаб кормит твой код данными; мок следит, как твой код с ним обращается. Нужны данные — бери стаб; нужно убедиться, что случился побочный эффект, — бери мок.

_Diagram: Стаб подаёт данные, и тест проверяет вывод твоего кода. Мок записывает вызов, и тест проверяет сам вызов._

#### Stub (стаб, заглушка)

- **Что это:** Подделка, которая возвращает заготовленный ответ.
- **Пример:** `getUser()` всегда возвращает `{ name: 'Ann' }`.
- **Что проверяем:** Вывод твоего кода при таких данных.
- **Когда брать:** Когда коду нужно что-то _получить_ от зависимости.

#### Mock (мок)

- **Что это:** Подделка, которая запоминает вызовы и даёт их проверить.
- **Пример:** Проверить, что `sendEmail` вызван ровно один раз с нужным адресом.
- **Что проверяем:** Сам вызов: был ли, сколько раз, с какими аргументами.
- **Когда брать:** Когда важен побочный эффект: письмо ушло, платёж отправлен.

### Over-mocking — моки, которые врут

> Репетировать пьесу целиком с картонными фигурами вместо всех остальных актёров: свои реплики ты отточишь, но так и не узнаешь, сыграет ли настоящая труппа вместе.

Подделки мощные, но в избытке опасны. Если ты мокаешь _всё_, тест проверяет твои допущения о том, как ведут себя другие части, — а не то, как они ведут себя на деле. Тест зеленеет, пока настоящая интеграция сломана. Это антипаттерн **чрезмерного моканья (over-mocking)**. Поэтому мокай медленные, внешние и непредсказуемые зависимости, но не подменяй подделкой ту самую вещь, которую пытаешься проверить. Тест, построенный целиком на моках, может пройти, пока настоящая система падает, — самый опасный вид зелёного.

_Diagram: В тесте всё замокано — и он зелёный. В продакшене настоящие база и API — и всё падает._

#### Over-mocking (чрезмерное моканье)

- **Симптом:** Тест зелёный, а в продакшене та же функция падает.
- **Причина:** Подделано всё вокруг — тест проверяет только твои допущения о других частях.
- **Пример:** Мок базы всегда отвечает «ок», а настоящая база отвергает запрос из-за неверного поля.
- **Лекарство:** Мокай только внешнее и медленное; стыки проверяй **Integration Test**.
- **Связи:** Злоупотребление **Test Double**; ломает доверие так же, как **Flaky Test**.

> **Вопрос мока:** «Я подделываю по-настоящему внешнюю зависимость, чтобы тест был быстрым и изолированным, — или я замокал столько, что тест проверяет только мои собственные допущения?»

> Моки и стабы подменяют настоящие зависимости, чтобы тестировать в изоляции. Стаб даёт данные, мок проверяет вызов. Замокаешь слишком много — тест пройдёт, пока реальность падает.

## 5. TDD и хорошие тесты

Кроме видов тестов есть ремесло — писать хорошие. И есть одна дисциплина, разработка через тесты, которая переворачивает привычный порядок и меняет то, как ты проектируешь код.

### TDD — сначала тест, потом код

> Нарисовать мишень до выстрела, а не после, — чтобы целиться в ясную цель, а не обводить яблочко вокруг того места, куда стрела случайно попала.

**Разработка через тесты (test-driven development, TDD)** переворачивает обычный порядок: ты пишешь тест _до_ кода. Цикл называется **red, green, refactor** (красный, зелёный, рефакторинг): напиши падающий тест на то, что хочешь (красный), напиши ровно столько кода, чтобы он прошёл (зелёный), потом приведи код в порядок под защитой теста (рефакторинг). Тест первым заставляет точно определить, что значит успех, ещё до постройки, и гарантирует, что код можно тестировать — ведь он родился из теста.

_Diagram: Цикл TDD: красный (тест падает) → зелёный (минимум кода) → рефакторинг (чистим, тесты зелёные) → снова._

#### Test-Driven Development (TDD, red-green-refactor)

- **Проблема:** Тесты пишут «потом» — и код выходит неудобным для тестирования.
- **Идея:** Сначала падающий тест, потом минимум кода, потом уборка. И так по кругу.
- **Пример:** Тест «пустая корзина → итог 0» падает; пишешь `total()`; тест зелёный; чистишь код.
- **Цена:** Непривычно и поначалу медленнее; сложно, когда задача ещё не ясна.
- **Когда брать:** Для логики с понятными правилами: расчёты, валидация, исправление багов.
- **Связи:** Даёт тестируемый код и **Living Documentation**; рефакторинг держится на **Test Behaviour**.

### Test Behaviour, Not Implementation — проверяй что, а не как

> Судить повара по тому, вкусное ли блюдо, а не по тому, в какой руке он держал нож. Важен итог, а не личные детали того, как он к нему пришёл.

Важнейшая привычка для долгоживущих тестов: проверяй, **что** код делает, а не **как** он это делает — **поведение (behaviour)**, а не **реализацию (implementation)**. Проверяй, что `getDiscount` возвращает верную цену, а не что он вызвал три конкретных внутренних помощника по порядку. Тесты, привязанные к итогу, переживают рефакторинг: можно переписать всё нутро, а они всё ещё проходят. Тесты, привязанные к реализации, ломаются при каждой уборке кода — и люди привыкают им не доверять и удалять их. Проверяй поведение, и тест остаётся полезным.

_Diagram: Хороший тест смотрит на вход и выход getDiscount. Плохой тест лезет во внутренних помощников — и ломается при каждом рефакторинге._

#### Test Behaviour, Not Implementation (поведение, а не реализация)

- **Проблема:** Тесты ломаются от каждого рефакторинга, хотя поведение не менялось.
- **Идея:** Проверять только наблюдаемый итог через публичный интерфейс.
- **Пример:** Проверить, что `getDiscount` вернул 90, — а не какие помощники он вызвал внутри.
- **Цена:** Иногда сложнее добраться до краевого случая только через вход и выход.
- **Когда брать:** Всегда по умолчанию.
- **Связи:** Делает возможным рефакторинг в **TDD**; лишние **Mock** часто привязывают к реализации.

### Living Documentation — тесты как документация

> Хорошо написанный набор тестов читается как список обещаний: «при этом оно делает то». Это яснее любого комментария о том, как код должен себя вести.

Ясный набор тестов работает как **живая документация (living documentation)**. Каждый тест записывает обещание — «при пустой корзине итог равен нулю». И раз тесты бегут, эта документация не устаревает, как комментарии: если поведение меняется, тест ломается. Поэтому пиши тесты так, чтобы их читали: ясные имена, одно поведение на тест, очевидный arrange-act-assert. Новичок должен понять, что делает код, просто читая его тесты. Эта читаемость стоит столько же, сколько сама проверка.

#### Living Documentation (живая документация)

- **Проблема:** Комментарии и вики врут: код поменялся, а текст остался старым.
- **Идея:** Тесты — обещания, записанные кодом. Сломалось обещание — тест покраснел.
- **Пример:** Имя теста: «пустая корзина — итог равен нулю». Читается как спецификация.
- **Цена:** Нужно время на понятные имена и одно поведение на тест.
- **Когда брать:** Всегда — особенно в коде, который читают новички.
- **Связи:** Держится на **Arrange–Act–Assert** и **Test Behaviour**.

> **Вопрос TDD:** «Описывает ли этот тест поведение, которое я обещал, — читаемо и через итог, — чтобы он пережил рефакторинг и объяснил следующему человеку, что делает код?»

> Пиши тест первым (red-green-refactor), проверяй поведение, а не реализацию, и делай тест читаемым — тогда тесты переживают рефакторинг и работают документацией, которая не устаревает.

## 6. Покрытие и флакающие тесты

Одно число и одна напасть решают, можно ли на деле доверять набору тестов. Неверно пойми покрытие или терпи флакающие тесты — и зелёный прогон перестанет что-либо значить.

### Code Coverage — что протестировано, а не насколько хорошо

> Карта улиц, по которым проехал патруль: удобно заметить районы, куда никто не заезжал, но проехать мимо дома — не значит проверить замки.

**Покрытие кода (code coverage)** — это процент кода, который выполняется во время тестов. Это полезный ориентир, чтобы найти то, что не тестировано _совсем_, — улицы, где патруль не был. Но высокое покрытие не значит хорошие тесты: код может выполниться в тесте, который ничего толком не проверяет, — 100% покрытия при почти нулевой проверке. Используй покрытие, чтобы находить слепые пятна, а не как доказательство качества. Тронутая строка — не то же самое, что проверенная строка.

_Diagram: Покрытие 75%: выполнилось 6 строк из 8. Но по-настоящему проверены (есть assertion) только 3._

#### Code Coverage (покрытие кода)

- **Что это:** Доля строк (или веток) кода, которые выполнились во время тестов.
- **Чем полезно:** Показывает слепые пятна — код, который не запускал ни один тест.
- **Подвох:** Не меряет качество: строка может выполниться, но ни один assertion её не проверит.
- **Пример:** 80% покрытия, но вся логика скидок в непокрытых 20% — вот где искать риск.
- **Когда брать:** Как фонарик для поиска пробелов, а не как цель.
- **Связи:** Превращается в ловушку по **закону Гудхарта**; не заменяет **Risk-based Testing**.

### Goodhart's Law — не дай покрытию стать целью

> Платить рабочим за километр покрашенной разметки и смотреть, как они рисуют длинные бесполезные линии на пустырях, чтобы добить план. Метрика выросла, дороги безопаснее не стали.

Когда число становится целью, люди оптимизируют число, а не то, что оно мерило, — это **закон Гудхарта (Goodhart's law)**. Потребуй «100% покрытия» — и получишь тесты, написанные, чтобы тронуть строки, а не поймать баги: пустые тесты, которые ничего не проверяют и просто обыгрывают метрику. Покрытие — фонарик для поиска пробелов, а не кубок, который надо набить до максимума. Гонись за настоящей уверенностью — ловят ли тесты поломку на деле? — и пусть покрытие будет одной подсказкой из нескольких, а не самой целью.

_Diagram: Мера становится целью — и рождаются пустые тесты: число растёт, а баги остаются._

#### Goodhart's Law (закон Гудхарта)

- **Суть:** Когда мера становится целью, она перестаёт быть хорошей мерой.
- **В тестах:** Правило «без 100% покрытия не мёржим» рождает тесты без assertion — ради числа.
- **Пример:** Тест вызывает функцию и ничего не проверяет — покрытие растёт, защиты ноль.
- **Лекарство:** Спрашивать «поймает ли тест поломку?», а покрытие держать подсказкой.
- **Связи:** Главная ловушка **Code Coverage**.

### Flaky Test — флакающий тест хуже, чем никакого

> Пожарная сигнализация, которая срабатывает случайно и без причины: через неделю её все игнорируют, и когда начинается настоящий пожар, никто не двигается.

**Флакающий тест (flaky test)** — тот, что проходит и падает случайно, хотя код не менялся. Обычно виноваты тайминг, порядок запуска или скрытая зависимость. Флакающие тесты — яд: когда красный результат может быть просто шумом, люди перестают верить любому красному, и настоящий сбой пропускают со словами «наверное, опять флакает». Тест должен быть **детерминированным (deterministic)**: тот же код — тот же результат, каждый раз. Чини флакающие тесты или удаляй их; набор, которому не доверяешь, не защищает ничего.

_Diagram: Тот же код, шесть прогонов — и два случайных падения. После такого любой красный считают шумом._

#### Flaky Test (флакающий, нестабильный тест)

- **Что это:** Тест, который то проходит, то падает без изменения кода.
- **Причины:** Тайминг (sleep, гонки), порядок тестов, скрытая общая зависимость.
- **Пример:** Тест ждёт 100 мс ответа сервиса; на загруженной CI-машине иногда не дожидается.
- **Вред:** Учит команду игнорировать красный — и настоящий сбой проходит мимо.
- **Лекарство:** Сделать тест **deterministic** — или удалить. Терпеть флакающий тест нельзя.
- **Связи:** Чаще всего живёт в **E2E**; ломает доверие к **CI Gate**.

> **Вопрос доверия:** «Можно ли на деле доверять моему набору — покрытие как подсказка, а не цель, и ноль флакающих тестов, — чтобы зелёный прогон правда значил „можно выпускать“?»

> Покрытие показывает, что протестировано, а не насколько хорошо, — не делай его целью. А флакающий тест хуже, чем никакого: раз красный может быть шумом, игнорируют любой красный.

## 7. Применять хорошо

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

### Risk-based Testing — тестируй рискованное, а не всё

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

Не весь код заслуживает одинакового внимания. **Тестирование по рискам (risk-based testing)** — это сосредоточить усилия там, где баг был бы дорогим или вероятным: ключевая бизнес-логика, деньги, безопасность, хитрые краевые случаи, части, которые часто меняются. Простому коду с низким риском можно уделить меньше. Гнаться за тестами для всего — значит тратить силы и получить набор такой большой и медленный, что его никто не запускает. Целься в тесты, которые ловят сбои, о которых ты правда пожалел бы, а не в число: покрытие важного лучше покрытия всего.

_Diagram: Чем дороже сбой и чем вероятнее поломка, тем больше тестов. Деньги, безопасность и ключевая логика — в правом верхнем углу._

#### Risk-based Testing (тестирование по рискам)

- **Проблема:** Сил на тесты мало, а пытаться покрыть всё — значит размазать их тонко.
- **Идея:** Больше тестов там, где сбой дорогой или вероятный; меньше — на тривиальном.
- **Пример:** Расчёт платежей и права доступа — плотно; геттеры и простая вёрстка — по минимуму.
- **Цена:** Нужно честно оценивать риск, а не тестировать то, что легко.
- **Когда брать:** Всегда, когда решаешь, на что тратить время на тесты.
- **Связи:** Заменяет погоню за **Code Coverage**; плотнее всего — **Unit Test** на логике.

### CI Gate — тесты на каждом изменении, автоматически

> Турникет, который не откроется, пока билет не действителен: проверка встроена в ворота, а не зависит от того, вспомнит ли кто-то посмотреть.

Тесты защищают, только если они правда запускаются. Встрой их в **пайплайн (pipeline)**, чтобы они бежали автоматически на каждом изменении, и блокируй мёрж, если они падают, — это **CI-гейт (CI gate)** из курса о деплое. Именно это превращает тесты из того, что ты, может быть, запустишь, в гарантию, что ничего сломанного не попадёт в основную ветку. «Без тестов не выпускаем» — стандарт, который стоит держать: непротестированное изменение в продакшене — это ставка, которую ты не обязан был делать.

_Diagram: Каждое изменение проходит через CI: все тесты зелёные — мёрж; хоть один красный — мёрж заблокирован._

#### CI Gate (CI-гейт, проверка перед мёржем)

- **Проблема:** Тесты есть, но их забывают запускать — и сломанный код уходит в main.
- **Идея:** CI сам гоняет все тесты на каждом изменении и не пускает мёрж, пока они не зелёные.
- **Пример:** Pull request: CI запускает тесты, один упал — кнопка мёржа неактивна.
- **Цена:** Время ожидания CI; флакающие тесты здесь бьют больнее всего.
- **Когда брать:** В любом проекте, где больше одного изменения в неделю.
- **Связи:** Запускает весь **Test Pyramid**; не терпит **Flaky Test**; ловит **Regression** до пользователя.

### Как всё связано

> Оркестр: каждый инструмент важен, но музыка получается только вместе.

Понятия курса работают как одна система. **Пирамида** задаёт форму набора, **TDD** его пишет, **подделки** дают юнит-тестам изоляцию, а **CI-гейт** гоняет всё на каждом изменении — и это даёт главное: менять код без страха. Против работают **флакающие тесты** и **чрезмерное моканье** — они подрывают доверие к зелёному. А **покрытие** лишь подсказывает, где пробелы.

_Diagram: Пирамида, TDD и подделки строят набор тестов; CI-гейт гоняет его и даёт менять без страха. Флакающие тесты и лишние моки бьют по доверию, покрытие лишь подсказывает._

#### Прежде чем доверять набору тестов

- **Даёт ли он менять уверенно** — поймал бы он регрессию завтра?
- **Есть ли у него форма пирамиды** — много юнит, немного интеграционных, пара e2e?
- **Проверяют ли тесты поведение**, а не внутреннюю реализацию, которая ломается на рефакторинге?
- **Мокаются ли только настоящие внешние зависимости**, а не то, что под тестом?
- **Быстрый ли он и детерминированный** — нет флакающих тестов, которые подтачивают доверие?
- **Бежит ли он в CI** на каждом изменении, блокируя сломанные мёржи?

#### Признаки, что ты тестируешь хорошо

- Ты **меняешь код уверенно** и доверяешь набору поймать то, что сломал.
- Твои тесты образуют **пирамиду**: быстрые в основании, пара реалистичных сверху.
- Тесты проверяют **поведение** и переживают рефакторинг, а не ломаются на каждой уборке.
- Набор **быстрый и детерминированный**, а **флакающие** тесты ты чинишь или удаляешь.
- Тесты **бегут в CI** на каждом изменении, а покрытие для тебя — подсказка, а не цель.

## Шпаргалка: все 19 понятий

### Зачем мы тестируем

- **Automated Test** (автотест): Код, который проверяет код: ввод → вызов → assertion → pass или fail.
- **Regression** (регрессия): Изменение ломает то, что раньше работало, часто далеко от места правки.

### Юнит-тесты

- **Unit Test** (юнит-тест): Одна функция в изоляции, за миллисекунды. Их тысячи.
- **Arrange–Act–Assert** (AAA): Подготовь, сделай одно действие, проверь результат.

### Интеграция, e2e и пирамида

- **Integration Test** (интеграционный тест): Несколько настоящих частей вместе — проверка швов.
- **End-to-End Test** (E2E, сквозной): Вся система глазами пользователя. Реалистично, но медленно и хрупко.
- **Test Pyramid** (пирамида тестов): Много юнит, меньше интеграционных, пара e2e. Не рожок мороженого.

### Подделки

- **Test Double** (тестовый двойник): Подделка вместо медленной, платной или случайной зависимости.
- **Stub** (стаб): Возвращает заготовленный ответ — кормит код данными.
- **Mock** (мок): Запоминает вызовы — проверяет, что побочный эффект случился.
- **Over-mocking** (чрезмерное моканье): Замокано всё — тест зелёный, а настоящая система падает.

### TDD и хорошие тесты

- **TDD** (red-green-refactor): Сначала падающий тест, потом минимум кода, потом уборка.
- **Test Behaviour, Not Implementation** (поведение, а не реализация): Проверяй итог через публичный интерфейс — тест переживёт рефакторинг.
- **Living Documentation** (живая документация): Тесты — обещания кодом, которые не устаревают.

### Доверие к набору

- **Code Coverage** (покрытие): Сколько кода выполнилось в тестах. Фонарик для пробелов, не мера качества.
- **Goodhart's Law** (закон Гудхарта): Мера, ставшая целью, перестаёт мерить. 100% покрытия → пустые тесты.
- **Flaky Test** (флакающий тест): Падает случайно. Лекарство — deterministic-тест или удаление.

### Применять хорошо

- **Risk-based Testing** (тестирование по рискам): Больше тестов там, где сбой дорогой или вероятный.
- **CI Gate** (CI-гейт): Тесты на каждом изменении; красный — мёрж заблокирован.

> Хорошее тестирование — это суждение: тестируй рискованное, держи пирамиду, проверяй поведение, а не реализацию, и гоняй всё автоматически — чтобы зелёный набор правда значил «можно менять».

Конец экспресс-курса · 7 глав · 19 понятий

Дальше — практика: возьми одну важную функцию и напиши для неё пару юнит-тестов — arrange, act, assert, — потом намеренно сломай функцию и посмотри, как тест краснеет и говорит, что именно не так. Потом добавь тест на следующий баг, который найдёшь, — пусть набор растёт вдоль сбоев, которые реально случаются. Но держи одну мысль выше остальных: тесты не про доказательство, что сегодняшний код работает. Это страховка, которая даёт менять завтра без страха, — и именно эта свобода даёт хорошему софту расти дальше, а не медленно каменеть.
