Тестирование
JUnit 5, Arrange-Act-Assert, цикл TDD, тест-дублёры Mockito, нестабильные тесты, мутационное тестирование, контрактные и интеграционные тесты.
8 вопросов
JuniorТеорияОчень частоЧто даёт юнит-тесту приём структурирования Arrange-Act-Assert?
Что даёт юнит-тесту приём структурирования Arrange-Act-Assert?
Он делит тело теста на три блока: Arrange готовит фикстуру и входные данные, Act вызывает одно проверяемое поведение, Assert проверяет результат. Ровно один Act на тест означает, что падение указывает на одно конкретное поведение, а фиксированная форма позволяет прочесть незнакомый тест за секунды. Это соглашение, а не то, что JUnit навязывает.
Типичные ошибки
- ✗Втискивать несколько вызовов Act в один тест, из-за чего падение уже не называет одно поведение
- ✗Считать, что JUnit навязывает три блока, а не то, что это соглашение
- ✗Принимать проверки, разбросанные по подготовке, за блок Assert
Уточняющие вопросы
- →Почему в юнит-тесте стоит держать ровно один шаг Act?
- →Как поведенческая формулировка Given-When-Then ложится на Arrange-Act-Assert?
JuniorТеорияЧастоЧто делают @Test, @ParameterizedTest и @RepeatedTest в фреймворке тестирования JUnit 5?
Что делают @Test, @ParameterizedTest и @RepeatedTest в фреймворке тестирования JUnit 5?
@Test помечает один тестовый метод без аргументов. @ParameterizedTest запускает тот же метод по разу на каждый вход из объявленного источника — например @ValueSource или @CsvSource. @RepeatedTest(n) выполняет метод n раз. Ещё JUnit 5 создаёт новый экземпляр тестового класса на каждый тест, поэтому тесты не протекают состоянием друг в друга.
Типичные ошибки
- ✗Считать, что
@RepeatedTestповторяет упавший тест, пока тот не пройдёт - ✗Ждать, что
@ParameterizedTestзаработает без объявленного источника аргументов - ✗Полагать, что один экземпляр тестового класса общий для всех его тестов
Уточняющие вопросы
- →Какие источники аргументов могут питать
@ParameterizedTest? - →Чем
@BeforeEachотличается от@BeforeAllв жизненном цикле теста?
JuniorТеорияЧастоКаковы три шага цикла red-green-refactor в разработке через тесты TDD?
Каковы три шага цикла red-green-refactor в разработке через тесты TDD?
Red: пишем падающий тест на ещё не существующее поведение и убеждаемся, что он падает. Green: пишем простейший код, который заставляет его пройти, пусть и топорный. Refactor: приводим код и тест в порядок, пока набор зелёный, так что тесты работают страховкой. Затем повторяем малыми шагами — тест пишется первым и ведёт за собой дизайн.
Типичные ошибки
- ✗Писать сначала боевой код, а тест уже после
- ✗Пропускать шаг refactor, как только тест позеленел
- ✗Путать TDD с целевым покрытием вместо цикла проектирования
Уточняющие вопросы
- →Зачем нужно увидеть падение нового теста до того, как заставить его пройти?
- →Как написание теста первым формирует API класса, а не только его покрытие?
MiddleДебаггингЧастоКласс тестов JUnit 5 проходит локально, но в CI падает через раз — разберите причину
Класс тестов JUnit 5 проходит локально, но в CI падает через раз — разберите причину
Тесты делят изменяемое состояние через static-список и зависят от порядка выполнения, а JUnit 5 его не гарантирует. Если первым отработал createsOrder, то ORDERS.get(0) — сегодняшний заказ, он не просрочен, и проверка падает. В одиночку или в другом порядке тест проходит. Лечится переводом фикстуры на каждый тест (нестатическое поле, сбрасываемое в @BeforeEach) и проверкой заказа, созданного самим тестом, а не элемента с индексом 0.
Типичные ошибки
- ✗Считать, что JUnit выполняет тестовые методы в порядке объявления
- ✗Называть исправлением правило перезапуска — оно прячет общее состояние, а не убирает его
- ✗Делить фикстуру через
static-поле, чтобы «сэкономить» на подготовке
Уточняющие вопросы
- →Почему чтение системных часов — такой частый источник нестабильности и как его убрать?
- →Почему
@TestMethodOrderзаставит набор проходить, но не сделает его корректным?
MiddleТеорияЧастоЧем в библиотеке моков Mockito различаются mock, stub и spy?
Чем в библиотеке моков Mockito различаются mock, stub и spy?
Mock — сгенерированный поддельный объект: каждый метод отдаёт значение по умолчанию (null, 0, false), пока вы не сказали иначе, а его вызовы проверяются через verify. Stubbing — то, что делают с дублёром: when(x.f()).thenReturn(v) закрепляет ответ за одним вызовом. Spy оборачивает реальный экземпляр: незастабленные методы выполняют настоящую реализацию, подменяются только застабленные. Mock и spy — объекты, stubbing — навешиваемое поведение.
Типичные ошибки
- ✗Ждать, что незастабленный метод mock выполнит настоящую реализацию
- ✗Ждать, что незастабленный метод spy вернёт значение по умолчанию, а не настоящий результат
- ✗Писать
when(spy.f())на spy — это вызовет настоящий метод; спасаетdoReturn(...).when(spy).f()
Уточняющие вопросы
- →Почему
when(spy.method())вызывает настоящий метод, аdoReturn(...).when(spy).method()— нет? - →Когда рукописная подделка предпочтительнее mock из Mockito?
MiddleДизайнИногдаВ прошлую пятницу команда заказов переименовала JSON-поле customerId в customer_id в своём REST-ответе. Их собственные тесты были зелёными, они выкатились по плану, и два сервиса-потребителя — биллинг и уведомления — за считанные минуты посыпались в проде на null-pointer. Этими сервисами владеют шесть команд, каждая выкатывается по несколько раз в день, общего стенда нет, а сквозной e2e-набор, который поймал бы это, идёт 40 минут и сам настолько нестабилен, что его перезапускают до зелёного. Руководство хочет правило: производитель не должен выкатывать изменение, ломающее потребителя. Спроектируйте подход к тестированию: что именно проверяется, кто это пишет, где оно запускается в конвейере каждой команды и как оно роняет сборку производителя ДО выката, а не после.
В прошлую пятницу команда заказов переименовала JSON-поле customerId в customer_id в своём REST-ответе. Их собственные тесты были зелёными, они выкатились по плану, и два сервиса-потребителя — биллинг и уведомления — за считанные минуты посыпались в проде на null-pointer. Этими сервисами владеют шесть команд, каждая выкатывается по несколько раз в день, общего стенда нет, а сквозной e2e-набор, который поймал бы это, идёт 40 минут и сам настолько нестабилен, что его перезапускают до зелёного. Руководство хочет правило: производитель не должен выкатывать изменение, ломающее потребителя. Спроектируйте подход к тестированию: что именно проверяется, кто это пишет, где оно запускается в конвейере каждой команды и как оно роняет сборку производителя ДО выката, а не после.
Ввести контрактные тесты со стороны потребителя. Каждый потребитель на локальной заглушке производителя описывает, какие запросы он шлёт и какие поля ответа реально читает — это ожидание и есть контракт, публикуемый в брокер. Сборка производителя затем проигрывает все опубликованные контракты против настоящего API и падает, если читаемое потребителем поле переехало или исчезло, — переименование уронило бы сборку заказов ещё до выката. Общий стенд при этом не нужен.
Типичные ошибки
- ✗Позволить производителю писать контракт — тогда он проверяет то, что производитель отдаёт, а не то, что потребитель читает
- ✗Принимать схему OpenAPI за контрактный тест — ничего не падает, когда нужное потребителю поле исчезает
- ✗Пытаться купить совместимость раздутым e2e-набором: он медленный, нестабильный и находит поломку уже после выката
Уточняющие вопросы
- →Почему контракт должен фиксировать только читаемые потребителем поля, а не весь ответ производителя?
- →Как производителю безопасно добавить поле и почему это не является поломкой контракта?
MiddleТеорияИногдаЧто измеряет мутационное тестирование такого, чего не может покрытие строк?
Что измеряет мутационное тестирование такого, чего не может покрытие строк?
Инструмент вроде PIT вносит мелкие изменения в скомпилированный боевой код — меняет > на >=, подменяет возвращаемое значение, удаляет вызов — и перезапускает набор на каждом мутанте. Мутант, из-за которого упал какой-то тест, убит; не замеченный зелёным набором — выжил, а балл равен убитым/всего. Значит, измеряется, ловят ли ваши проверки неверное поведение, тогда как покрытие строк доказывает лишь факт выполнения строки: тест без проверок тоже даёт 100%.
Типичные ошибки
- ✗Считать 100% покрытия строк доказательством того, что тесты поймают регрессию
- ✗Думать, что инструмент меняет тесты, а не боевой код
- ✗Читать выжившего мутанта как ошибку в боевом коде, а не как пробел в проверках
Уточняющие вопросы
- →Почему набор может дать 100% покрытия строк и при этом почти не убивать мутантов?
- →Что такое эквивалентный мутант и почему он ограничивает достижимый балл?
MiddleДизайнИногдаВы ревьюите набор тестов сервиса оформления заказа перед релизом. Все его 400 тестов помечены @SpringBootTest: каждый поднимает полный контекст приложения, реальный контейнер PostgreSQL и HTTP-слой, набор идёт 38 минут — поэтому разработчики пушат, не запуская его, и поломку первым замечает CI. При чтении видно, что большинство тестов проверяют чистую логику: арифметику скидок, округление суммы корзины, правила истечения купонов. Меньшинство действительно зависит от инфраструктуры: маппинги JPA, откат по @Transactional и JSON-контракт REST-контроллера. Команда хочет обратную связь на PR быстрее пяти минут, не потеряв те ошибки, которые медленные тесты реально ловят. Какие тесты вы перепишете на обычные юнит-тесты с Mockito, какие обязаны остаться интеграционными, что даёт каждый слой и как удержать платёжного провайдера вне обоих?
Вы ревьюите набор тестов сервиса оформления заказа перед релизом. Все его 400 тестов помечены @SpringBootTest: каждый поднимает полный контекст приложения, реальный контейнер PostgreSQL и HTTP-слой, набор идёт 38 минут — поэтому разработчики пушат, не запуская его, и поломку первым замечает CI. При чтении видно, что большинство тестов проверяют чистую логику: арифметику скидок, округление суммы корзины, правила истечения купонов. Меньшинство действительно зависит от инфраструктуры: маппинги JPA, откат по @Transactional и JSON-контракт REST-контроллера. Команда хочет обратную связь на PR быстрее пяти минут, не потеряв те ошибки, которые медленные тесты реально ловят. Какие тесты вы перепишете на обычные юнит-тесты с Mockito, какие обязаны остаться интеграционными, что даёт каждый слой и как удержать платёжного провайдера вне обоих?
Делить по тому, что тесту действительно нужно. Чистой логике — скидкам, округлению, правилам купонов — незачем поднимать Spring: создайте класс, замокайте соседей, тест отработает за миллисекунды. Это и есть основная масса из 400. Интеграционным тест остаётся только там, где проверяется сама обвязка: маппинги JPA, откат транзакции, JSON-контракт контроллера — замоканный репозиторий не способен уронить маппинг. Платёжный провайдер остаётся вне обоих слоёв, подменённый на своей границе.
Типичные ошибки
- ✗Брать
@SpringBootTestпо умолчанию, из-за чего каждый тест платит за контекст, который ему не нужен - ✗Замокать
EntityManagerи считать, что маппинг JPA этим покрыт - ✗Позволить интеграционному тесту дёрнуть настоящего платёжного провайдера — набор попадает в зависимость от третьей стороны
Уточняющие вопросы
- →Почему замоканный репозиторий никогда не поймает сломанный маппинг
@OneToMany? - →Когда срезанный тест (
@DataJpaTest,@WebMvcTest) — верная середина?