TDD и практики тестирования
Тест бывает не только полезным или бесполезным - он бывает вредным. Вредный тест это тот, который падает при каждом безобидном изменении кода и не падает, когда логика действительно сломалась. Он потребляет время на поддержку и создаёт ложное ощущение защищённости.
Поэтому на собесе спрашивают не про синтаксис, а про суждение: что проверять, сколько покрывать, что делать с тестами, которые падают через раз.
// Формулировки: «что такое TDD и зачем он?», «гонитесь ли вы за стопроцентным покрытием?», «что делать с нестабильным тестом?», «как проверять поведение, а не реализацию?».
Цикл разработки через тесты и структура теста
TDD (test-driven development, разработка через тесты) - цикл из трёх шагов. Красный: пишешь падающий тест на ещё не написанное поведение. Зелёный: пишешь минимальный код, чтобы тест прошёл. Рефакторинг: приводишь код в порядок, а тесты держат тебя от поломки.
Ценность тут не в самой дисциплине, а в двух побочных эффектах. Первый: сначала описав, как код будет использоваться, ты получаешь более удобный интерфейс, потому что смотришь на него глазами вызывающего. Второй: тест точно проверяет что-то реальное - ты видел его падающим. Тест, написанный после кода и ни разу не падавший, вполне может проверять пустоту.
// Любой тест удобно строить в три части: подготовка данных, действие, проверка. Одно действие на тест. Название пишут по-человечески: «возвращает ошибку при отрицательной сумме» вместо «тест1». Когда такой тест падает в отчёте, причина понятна без чтения кода.
- цикл красный-зелёный-рефакторинг
- падающий тест, минимальный код, наведение порядка
- подготовка-действие-проверка
- три части теста, одно действие на тест
Поведение против реализации
Хрупкий тест получается, когда проверяют внутреннее устройство: какие методы в каком порядке позвались, какие поля стали какими. Переписал реализацию, поведение сохранил - тест всё равно красный. Такие тесты со временем начинают мешать рефакторингу, и их отключают, а не чинят.
Здоровый тест проверяет наблюдаемое поведение: на такой вход получился такой результат, такое состояние, такой вызов наружу. Внутренности при этом можно переписать целиком, и тест останется зелёным - именно это делает его полезным при рефакторинге.
// Отсюда мера: проверка вызовов уместна там, где вызов и есть результат работы - письмо отправлено, сообщение опубликовано, деньги списаны. Проверять, что сервис позвал у себя три внутренних метода, смысла нет: это и есть проверка реализации.
- хрупкий тест
- падает от изменения устройства, хотя поведение прежнее
Покрытие и нестабильные тесты
Покрытие показывает, какие строки исполнялись во время прогона. Это метрика ИСПОЛНЕНИЯ, а не проверки: тест, который дёргает метод и ничего не проверяет, даёт стопроцентное покрытие и нулевую пользу. Поэтому целью её не делают - её используют, чтобы найти непокрытые куски и решить, важны ли они.
Нестабильный тест - тот, что падает через раз без изменений в коде. Он опаснее упавшего: команда привыкает перезапускать прогон, и настоящее падение тонет в шуме. Типичные причины: зависимость от текущего времени, от порядка тестов, от общих данных, гонки в многопоточном коде, ожидание фиксированной паузой вместо ожидания условия.
// Правильная реакция - вынести такой тест из основного прогона и завести на него задачу, а не перезапускать. И помнить, что гонка в тесте нередко означает гонку в самом коде: у меня в замерах гонки проявлялись через раз - один прогон из трёх выдавал правильный результат при полностью сломанной синхронизации.
- покрытие
- какие строки исполнялись, а не что было проверено
- нестабильный тест
- падает через раз без изменений кода, приучает игнорировать прогон
Как отвечать: «Гонитесь ли вы за стопроцентным покрытием?»
Нет, потому что покрытие меряет исполнение строк, а не проверку поведения. Тест, который просто вызывает метод и ничего не утверждает, даёт стопроцентное покрытие и нулевую пользу - под такую метрику легко подогнать отчёт, не улучшив ничего. Я использую покрытие как подсказку: смотрю, какие куски не покрыты, и решаю по каждому, важен он или нет. Обработка ошибок, граничные случаи и денежная логика должны быть покрыты обязательно; на геттерах и на классах с одними данными настаивать бессмысленно. Куда полезнее следить за другими вещами: падает ли сборка при провале теста, нет ли нестабильных тестов, проверяют ли тесты поведение, а не устройство. Разумный ориентир - высокое покрытие критичных модулей вместо единого числа по всему проекту.
Почему это сильный ответ: объяснено, что именно меряет покрытие, показано, как метрика ломается при превращении в цель, и предложены более осмысленные ориентиры.
На чём валят
- −Делать покрытие целью. Оно меряет исполнение строк: тест без единой проверки даёт сто процентов.
- −Проверять порядок внутренних вызовов. Такой тест краснеет от рефакторинга и не ловит поломок поведения.
- −Перезапускать прогон, когда тест падает через раз. Настоящее падение утонет в привычном шуме.
- −Ждать фиксированной паузой в тестах на многопоточность. На загруженной машине пауза не спасёт, а прогон замедлится.
- −Проверять несколько действий в одном тесте. При падении непонятно, какое из них сломалось.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 14, остальные разбираются в тренажёре.
- Что описывает структура AAA (given-when-then) одного теста?A)Три обязательных ассерта в конце тестаB)Arrange (подготовка) → Act (действие) → Assert (проверка)C)Три вложенных try/catch для разных исключенийD)Три отдельных теста на один метод
показать ответ и разбор
+B)Arrange (подготовка) → Act (действие) → Assert (проверка)// разбор: AAA раскладывает тест на три фазы: Arrange — подготовить данные и окружение, Act — выполнить проверяемое действие, Assert — проверить результат. Аналог — given-when-then. Структура держит тест читаемым и сфокусированным на одном сценарии; смешение фаз (проверки посреди подготовки) — запах. Число ассертов при этом не фиксировано.
- Принцип FIRST требует, чтобы тесты были Independent. Что это значит?A)Тесты не зависят от бизнес-требованийB)Тесты пишет независимая команда, а не автор кодаC)Каждый тест не зависит от других: порядок и результат соседей не влияютD)Каждый тест проверяет независимый микросервис
показать ответ и разбор
+C)Каждый тест не зависит от других: порядок и результат соседей не влияют// разбор: Independent в FIRST (Fast, Independent, Repeatable, Self-validating, Timely) — тесты не связаны: не делят состояние, не полагаются на порядок, каждый готовит своё окружение сам. Тогда падение одного не роняет каскад, а любой тест можно гонять в одиночку. Зависимость от соседа даёт порядко-зависимые флаки и мешает параллели.
- Почему тест стоит писать против поведения, а не реализации?A)Тесты реализации быстрее компилируютсяB)Иначе не получить 100% покрытие строкC)Поведение проще замокать, чем реализациюD)Так тест не краснеет при рефакторинге, не меняющем наблюдаемый результат
показать ответ и разбор
+D)Так тест не краснеет при рефакторинге, не меняющем наблюдаемый результат// разбор: Тест поведения проверяет наблюдаемый результат через публичный контракт: вход-выход, эффект на границе. Он переживает рефакторинг внутренностей — переименовал приватный метод, изменил порядок вызовов, а тест зелёный, ведь поведение то же. Тест реализации (на приватные методы, порядок вызовов) краснеет от любого такого изменения — это хрупкость.
- Тесты рефлексией дёргают приватные методы класса. Чем это аукнется?A)Тесты хрупки: рефакторинг приватных внутренностей ломает их без смены поведенияB)Ничем: приватные методы тоже надо покрывать напрямуюC)Тесты станут быстрее за счёт обхода публичного APID)Рефлексия сделает тесты потокобезопасными
показать ответ и разбор
+A)Тесты хрупки: рефакторинг приватных внутренностей ломает их без смены поведения// разбор: Приватные методы — деталь реализации. Тест, привязанный к ним через рефлексию, краснеет при любом переименовании или реструктуризации, хотя публичное поведение не изменилось — классическая хрупкость, которая душит рефакторинг. Приватную логику проверяют косвенно, через публичный контракт, который её задействует; если это трудно — сигнал вынести логику в отдельный класс.
- Покрытие 95%, но баги регулярно доезжают до прода. Как так?A)95% — слишком мало, нужно строго 100%B)Coverage измеряет исполненные строки, а не силу ассертовC)Покрытие посчитано неверно, инструмент замера покрытия попросту врётD)Баги прячутся в тех 5%, что не покрыты
показать ответ и разбор
+B)Coverage измеряет исполненные строки, а не силу ассертов// разбор: Coverage говорит лишь, что строка исполнилась во время теста, — но не что её результат проверен. Тест может прогнать код и почти ничего не заассертить: строки «покрыты», поведение — нет. Отсюда высокий процент при слабых тестах. Целиться надо в проверку важных веток, границ и контрактов, а не в саму цифру покрытия.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.