Экспресс-курс · No. 21

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

Только суть · Один образ на идею · Выучи слова

§ 01

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

Тест — это код, что проверяет код

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

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

Настоящий смысл — менять без страха

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

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

Они ловят регрессии автоматически

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

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

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

§ 02

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

Тестируй один маленький кусок в изоляции

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

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

Быстро и много — вот вся идея

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

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

Arrange, act, assert

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

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

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

§ 03

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

Интеграционные тесты проверяют куски, работающие вместе

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

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

Сквозные тесты используют всю систему как пользователь

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

Сквозной тест (end-to-end, e2e) гоняет всю систему так, как делал бы реальный пользователь, — кликни кнопку, заполни форму, проверь, что нужное случилось через весь стек. Это самый реалистичный тест и самое ценное подтверждение, что продукт реально работает. Но он же самый медленный и хрупкий: много частей должны все работать, а маленькое изменение UI может его сломать. Ты используешь e2e-тесты для тех немногих критичных путей, а не для всего.

Пирамида тестов их балансирует

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

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

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

§ 04

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

Замени реальную зависимость подделкой

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

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

Стаб даёт заготовленные ответы; мок проверяет вызов

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

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

Чрезмерное моканье делает тесты, что врут

Репетировать пьесу целиком с картонными заменами всех остальных актёров — ты можешь идеально отбить свои реплики и всё равно не знать, сработает ли реальный состав вместе.

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

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

§ 05

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

TDD: пиши тест первым

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

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

Тестируй поведение, а не реализацию

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

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

Хорошие тесты — это документация

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

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

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

§ 06

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

Покрытие меряет, что протестировано, а не насколько хорошо

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

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

Не дай покрытию стать целью

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

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

Флакающий тест хуже, чем никакого

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

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

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

§ 07

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

Тестируй рискованные части, а не всё

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

Не весь код заслуживает равного тестирования. Сосредоточь усилие там, где баг был бы дорог или вероятен: ядровая бизнес-логика, деньги, безопасность, хитрые краевые случаи, части, что часто меняются. Тривиальный, малорисковый код может получить меньше. Гнаться за тестами для всего тратит усилие и создаёт набор такой большой и медленный, что никто его не гоняет. Целься в тесты, что ловят сбои, о которых ты бы правда пожалел, а не в число — покрытие важного бьёт покрытие всего.

Гоняй тесты автоматически, на каждом изменении

Турникет, что не откроется, пока твой билет не валиден, — проверка встроена в ворота, а не оставлена на то, помнит ли кто-то посмотреть.

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

Прежде чем доверять набору тестов
  • Даёт ли он менять уверенно — поймал бы он регрессию завтра? - Имеет ли он форму пирамиды — много юнит, немного интеграционных, пара e2e? - Проверяют ли тесты поведение, а не внутреннюю реализацию, что ломается на рефакторе? - Используются ли моки для реальных внешних зависимостей, а не подделывают то, что под тестом? - Быстр ли он и детерминирован — нет флакающих тестов, что подтачивают доверие? - Бежит ли он в CI на каждом изменении, блокируя сломанные мёржи?
Слова, которыми ты теперь владеешь
  • test / pass / fail / assertion — проверка кода, её исходы и сама проверка. - регрессия (regression) — изменение, ломающее то, что раньше работало. - unit / integration / end-to-end (e2e) — один кусок, куски вместе, вся система. - пирамида тестов — много быстрых юнит-тестов, меньше интеграционных, пара e2e. - mock / stub — подделка, что проверяет вызов, подделка, что даёт данные. - TDD / red-green-refactor — писать тест первым, в цикле. - coverage / flaky / deterministic — что протестировано, ловушка случайных падений и лекарство.
Признаки, что ты тестируешь хорошо
  • Ты меняешь код уверенно, доверяя набору поймать то, что сломал. - Твои тесты образуют пирамиду — быстрые в основании, пара реалистичных сверху. - Тесты проверяют поведение и переживают рефакторы, а не ломаются на каждой уборке. - Набор быстр и детерминирован, и ты чинишь или удаляешь флакающие тесты. - Тесты бегут в CI на каждом изменении, и ты считаешь покрытие подсказкой, а не целью.

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

Конец экспресс-курса · 7 глав · выучи слова

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