сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Тестирование на Java

TDD и практики тестирования

Практики: как отличить полезный тест от вредного

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

Поэтому на собесе спрашивают не про синтаксис, а про суждение: что проверять, сколько покрывать, что делать с тестами, которые падают через раз.

// Формулировки: «что такое TDD и зачем он?», «гонитесь ли вы за стопроцентным покрытием?», «что делать с нестабильным тестом?», «как проверять поведение, а не реализацию?».

Цикл разработки через тесты и структура теста

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

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

// Любой тест удобно строить в три части: подготовка данных, действие, проверка. Одно действие на тест. Название пишут по-человечески: «возвращает ошибку при отрицательной сумме» вместо «тест1». Когда такой тест падает в отчёте, причина понятна без чтения кода.

цикл красный-зелёный-рефакторинг
падающий тест, минимальный код, наведение порядка
подготовка-действие-проверка
три части теста, одно действие на тест

Поведение против реализации

Хрупкий тест получается, когда проверяют внутреннее устройство: какие методы в каком порядке позвались, какие поля стали какими. Переписал реализацию, поведение сохранил - тест всё равно красный. Такие тесты со временем начинают мешать рефакторингу, и их отключают, а не чинят.

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

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

хрупкий тест
падает от изменения устройства, хотя поведение прежнее

Покрытие и нестабильные тесты

Покрытие показывает, какие строки исполнялись во время прогона. Это метрика ИСПОЛНЕНИЯ, а не проверки: тест, который дёргает метод и ничего не проверяет, даёт стопроцентное покрытие и нулевую пользу. Поэтому целью её не делают - её используют, чтобы найти непокрытые куски и решить, важны ли они.

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

// Правильная реакция - вынести такой тест из основного прогона и завести на него задачу, а не перезапускать. И помнить, что гонка в тесте нередко означает гонку в самом коде: у меня в замерах гонки проявлялись через раз - один прогон из трёх выдавал правильный результат при полностью сломанной синхронизации.

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

Как отвечать: «Гонитесь ли вы за стопроцентным покрытием?»

Нет, потому что покрытие меряет исполнение строк, а не проверку поведения. Тест, который просто вызывает метод и ничего не утверждает, даёт стопроцентное покрытие и нулевую пользу - под такую метрику легко подогнать отчёт, не улучшив ничего. Я использую покрытие как подсказку: смотрю, какие куски не покрыты, и решаю по каждому, важен он или нет. Обработка ошибок, граничные случаи и денежная логика должны быть покрыты обязательно; на геттерах и на классах с одними данными настаивать бессмысленно. Куда полезнее следить за другими вещами: падает ли сборка при провале теста, нет ли нестабильных тестов, проверяют ли тесты поведение, а не устройство. Разумный ориентир - высокое покрытие критичных модулей вместо единого числа по всему проекту.

Почему это сильный ответ: объяснено, что именно меряет покрытие, показано, как метрика ломается при превращении в цель, и предложены более осмысленные ориентиры.

На чём валят

  • Делать покрытие целью. Оно меряет исполнение строк: тест без единой проверки даёт сто процентов.
  • Проверять порядок внутренних вызовов. Такой тест краснеет от рефакторинга и не ловит поломок поведения.
  • Перезапускать прогон, когда тест падает через раз. Настоящее падение утонет в привычном шуме.
  • Ждать фиксированной паузой в тестах на многопоточность. На загруженной машине пауза не спасёт, а прогон замедлится.
  • Проверять несколько действий в одном тесте. При падении непонятно, какое из них сломалось.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 14, остальные разбираются в тренажёре.

  1. #test_practices_tdd1 / 5
    Что описывает структура AAA (given-when-then) одного теста?
    A)Три обязательных ассерта в конце теста
    B)Arrange (подготовка) → Act (действие) → Assert (проверка)
    C)Три вложенных try/catch для разных исключений
    D)Три отдельных теста на один метод
    показать ответ и разбор
    +B)Arrange (подготовка) → Act (действие) → Assert (проверка)

    // разбор: AAA раскладывает тест на три фазы: Arrange — подготовить данные и окружение, Act — выполнить проверяемое действие, Assert — проверить результат. Аналог — given-when-then. Структура держит тест читаемым и сфокусированным на одном сценарии; смешение фаз (проверки посреди подготовки) — запах. Число ассертов при этом не фиксировано.

  2. #test_practices_tdd2 / 5
    Принцип FIRST требует, чтобы тесты были Independent. Что это значит?
    A)Тесты не зависят от бизнес-требований
    B)Тесты пишет независимая команда, а не автор кода
    C)Каждый тест не зависит от других: порядок и результат соседей не влияют
    D)Каждый тест проверяет независимый микросервис
    показать ответ и разбор
    +C)Каждый тест не зависит от других: порядок и результат соседей не влияют

    // разбор: Independent в FIRST (Fast, Independent, Repeatable, Self-validating, Timely) — тесты не связаны: не делят состояние, не полагаются на порядок, каждый готовит своё окружение сам. Тогда падение одного не роняет каскад, а любой тест можно гонять в одиночку. Зависимость от соседа даёт порядко-зависимые флаки и мешает параллели.

  3. #test_practices_tdd3 / 5
    Почему тест стоит писать против поведения, а не реализации?
    A)Тесты реализации быстрее компилируются
    B)Иначе не получить 100% покрытие строк
    C)Поведение проще замокать, чем реализацию
    D)Так тест не краснеет при рефакторинге, не меняющем наблюдаемый результат
    показать ответ и разбор
    +D)Так тест не краснеет при рефакторинге, не меняющем наблюдаемый результат

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

  4. #test_practices_tdd4 / 5
    Тесты рефлексией дёргают приватные методы класса. Чем это аукнется?
    A)Тесты хрупки: рефакторинг приватных внутренностей ломает их без смены поведения
    B)Ничем: приватные методы тоже надо покрывать напрямую
    C)Тесты станут быстрее за счёт обхода публичного API
    D)Рефлексия сделает тесты потокобезопасными
    показать ответ и разбор
    +A)Тесты хрупки: рефакторинг приватных внутренностей ломает их без смены поведения

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

  5. #test_practices_tdd5 / 5
    Покрытие 95%, но баги регулярно доезжают до прода. Как так?
    A)95% — слишком мало, нужно строго 100%
    B)Coverage измеряет исполненные строки, а не силу ассертов
    C)Покрытие посчитано неверно, инструмент замера покрытия попросту врёт
    D)Баги прячутся в тех 5%, что не покрыты
    показать ответ и разбор
    +B)Coverage измеряет исполненные строки, а не силу ассертов

    // разбор: Coverage говорит лишь, что строка исполнилась во время теста, — но не что её результат проверен. Тест может прогнать код и почти ничего не заассертить: строки «покрыты», поведение — нет. Отсюда высокий процент при слабых тестах. Целиться надо в проверку важных веток, границ и контрактов, а не в саму цифру покрытия.

дальше

Теорию прочитали. Навык ставится повторением

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