JUnit 5: основы
Написал класс с двумя тестами и полем-счётчиком, который каждый тест увеличивает на единицу. Оба теста напечатали counter=1. Проверил идентичность объектов - их оказалось ДВА: под каждый тест JUnit создаёт новый экземпляр класса. А ещё второй тест выполнился раньше первого, хотя в коде объявлен ниже.
Оба факта - основа изоляции. Тесты не должны влиять друг на друга, поэтому состояние обнуляется, а порядок тебе не обещают. Спрашивают на собесе именно это: жизненный цикл, изоляцию и умение проверять исключения.
// Формулировки: «опиши жизненный цикл теста», «зачем @BeforeEach, если есть конструктор?», «как проверить, что метод бросает исключение?», «в каком порядке выполняются тесты?».
Жизненный цикл и почему он такой
Порядок из моего замера, полностью: BeforeAll один раз на весь класс, потом на КАЖДЫЙ тест - BeforeEach, сам тест, AfterEach, и в конце AfterAll. Между тестами создаётся новый объект класса, поэтому поля начинаются с чистого листа.
Отсюда ответ на вопрос «зачем @BeforeEach, если есть конструктор». Конструктор тоже вызывается перед каждым тестом, но в нём ещё не отработали расширения фреймворка: не внедрены зависимости, не подставлены моки. @BeforeEach выполняется позже и видит уже собранную обстановку.
// BeforeAll и AfterAll статические именно потому, что относятся ко всему классу, а не к экземпляру - экземпляров-то много. В них кладут дорогое и общее: поднять контейнер с базой, прочитать большой файл. Всё, что должно быть свежим у каждого теста, живёт в BeforeEach.
BeforeAll
BeforeEach
тест 2, counter=1
AfterEach
BeforeEach
тест 1, counter=1
AfterEach
AfterAll
разных экземпляров класса теста: 2- @BeforeEach / @BeforeAll
- перед каждым тестом / один раз на класс, поэтому статический
- изоляция
- новый экземпляр на каждый тест, поля не переносятся
Проверки, которые стоит знать
Сравнение сложных значений работает по содержимому, и сообщение об ошибке само говорит, что не сошлось: у меня сравнение двух списков дало expected: <[1, 2, 3]> but was: <[1, 3, 2]>. Отсюда правило: сравнивай целые объекты, а не разбирай их по полям вручную - тогда провал сразу читаем.
Обычная цепочка проверок останавливается на первой неудаче, и ты видишь одну проблему из трёх. Проверка группой это меняет: в моём замере она выполнила обе проверки и выдала Multiple Failures (2 failures) с обоими сообщениями сразу. Удобно, когда проверяешь несколько полей одного результата.
Исключения проверяют не через try-catch с пометкой о провале, а специальной конструкцией: она выполняет код, требует, чтобы исключение вылетело, и ВОЗВРАЩАЕТ его. Дальше можно проверить сообщение и причину. Без возврата тест ловил бы сам факт исключения, но не его содержание.
var e = assertThrows(IllegalArgumentException.class,
() -> service.pay(-100));
assertEquals("сумма должна быть положительной", e.getMessage());
// сравнение списков при провале:
// expected: <[1, 2, 3]> but was: <[1, 3, 2]>
// группа проверок:
// Multiple Failures (2 failures) ... первая ... вторая- assertThrows
- требует исключение и возвращает его для дальнейших проверок
- assertAll
- выполняет все проверки и показывает все провалы разом
Порядок, параметризация и общая обстановка
Мой замер показал, что тесты выполняются не в порядке объявления. Это сделано намеренно: порядок стабилен между запусками, но специально не совпадает с исходным кодом, чтобы никто не начал на него закладываться. Тест, который работает только после соседа, - сломанный тест.
Параметризованный тест избавляет от копирования: один метод, а данные приходят набором - списком значений, парами вход-ожидание или из файла. Вместо пяти почти одинаковых методов получается один и пять строк данных, причём в отчёте каждая строка видна отдельно.
// Расширения - способ вынести общую обстановку: поднять базу, подставить фиксированное время, подчистить за тестом. Своё расширение подключается аннотацией к классу и работает у всех тестов внутри, поэтому одинаковую подготовку не приходится копировать по проекту.
- параметризованный тест
- один метод, набор данных, каждая строка отдельно в отчёте
- расширение
- общая подготовка и уборка, подключаемая к классу теста
Как отвечать: «Как проверить, что метод бросает исключение?»
Специальной конструкцией assertThrows: передаю ожидаемый тип и код в виде лямбды. Она проверяет, что исключение действительно вылетело и что оно нужного типа, а главное - возвращает сам объект исключения. Дальше я проверяю сообщение и причину, потому что «упало хоть что-то» это слабая проверка: метод мог свалиться совсем по другому поводу и тест всё равно был бы зелёным. Ловить исключение вручную через try-catch я не стал бы: тогда легко забыть пометить провалом случай, когда исключения НЕ было, и тест начнёт молча проходить всегда. И ещё слежу, чтобы в лямбде была ровно одна операция, которая должна упасть, иначе непонятно, какая именно строчка бросила исключение.
Почему это сильный ответ: назван инструмент, объяснено, зачем нужен возвращаемый объект, и разобрана ошибка ручного try-catch, из-за которой тест перестаёт что-либо проверять.
На чём валят
- −Закладываться на порядок тестов. У меня второй тест выполнился раньше первого - порядок специально не совпадает с исходным кодом.
- −Переносить состояние между тестами через поле. На каждый тест создаётся новый экземпляр, поле обнулится - я это проверял счётчиком.
- −Ловить исключение вручную и забыть про случай, когда его не было. Тест станет зелёным навсегда.
- −Проверять только тип исключения. Метод мог упасть по другой причине, а тест этого не заметит.
- −Разбирать результат по полям вместо сравнения целиком. Сообщение о провале перестаёт объяснять, что именно разошлось.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 15, остальные разбираются в тренажёре.
- Тест кладёт значение в поле объекта, а следующий тест ожидает его там же — но поле пустое. Почему?A)Поле следовало объявить volatile, иначе его значение не видно другому тест-методуB)JUnit сбрасывает все поля в null между тестами через рефлексиюC)JUnit по умолчанию создаёт новый экземпляр тест-класса для каждого тест-методаD)Тесты выполнились в неправильном порядке — помог бы @Order
показать ответ и разбор
+C)JUnit по умолчанию создаёт новый экземпляр тест-класса для каждого тест-метода// разбор: По умолчанию JUnit 5 создаёт НОВЫЙ экземпляр тест-класса на каждый тест-метод (per-method) — это изоляция: состояние в полях не течёт между тестами. Рассчитывать на общее поле нельзя; общая подготовка идёт в @BeforeEach (свежая на каждый) или в static-поле/@BeforeAll. Осознанно делить экземпляр — @TestInstance(PER_CLASS).
- Для чего используется @ParameterizedTest?A)Чтобы запустить тест в нескольких потоках параллельноB)Чтобы пометить тест как медленный и вынести его в отдельную стадию CIC)Чтобы передать тесту зависимости через конструктор прямо из Spring-контекстаD)Чтобы прогнать одну тестовую логику на наборе входных данных без копипасты
показать ответ и разбор
+D)Чтобы прогнать одну тестовую логику на наборе входных данных без копипасты// разбор: @ParameterizedTest гоняет один метод на множестве входов — данные дают @ValueSource, @CsvSource, @EnumSource или @MethodSource. Вместо пяти почти одинаковых тестов — один с таблицей случаев, включая граничные. Каждый набор — отдельный прогон с понятным именем в отчёте.
- В assertEquals(a, b) какой аргумент трактуется как ожидаемое значение?A)Первый (a) — ожидаемое, второй — фактическое; от порядка зависит текст ошибкиB)Порядок аргументов не важен: assertEquals симметричен и сравнивает через equalsC)Второй (b) — ожидаемое, первый — фактическоеD)JUnit сам определяет ожидаемое по типу: константа считается expected
показать ответ и разбор
+A)Первый (a) — ожидаемое, второй — фактическое; от порядка зависит текст ошибки// разбор: Сигнатура — assertEquals(expected, actual): первым идёт ожидаемое, вторым фактическое. Само сравнение симметрично (equals), но при провале JUnit печатает «expected: X but was: Y» по позициям. Перепутал порядок — сообщение вводит в заблуждение при отладке, хотя тест краснеет там же.
- Тест-класс с per-method жизненным циклом не компилируется/падает на старте. Что не так?
class OrderTest { @BeforeAll void initDb() { /* дорогая подготовка один раз */ } @Test void createsOrder() { /* ... */ } }A)У тест-класса нет модификатора public — JUnit 5 его не увидитB)@BeforeAll-метод не static — без @TestInstance(PER_CLASS) он обязан быть staticC)@Test-метод не бросает Exception, поэтому @BeforeAll не связывается с ним по жизненному циклуD)Держать @BeforeAll и @Test в одном классе без @Nested запрещенопоказать ответ и разбор
+B)@BeforeAll-метод не static — без @TestInstance(PER_CLASS) он обязан быть static// разбор: При дефолтном жизненном цикле (PER_METHOD) экземпляра класса ещё нет в момент @BeforeAll, поэтому метод обязан быть static — иначе JUnit не может его вызвать. Либо делаем initDb static, либо ставим на класс @TestInstance(Lifecycle.PER_CLASS) и тогда нестатический @BeforeAll допустим. Public классу в JUnit 5 не нужен.
- Тест проверяет три поля ответа тремя assertEquals подряд. Что даёт замена их на assertAll?A)assertAll делает проверки атомарными: при провале откатывает уже прошедшиеB)assertAll ускоряет тест, выполняя проверки параллельноC)assertAll сообщает про ВСЕ провалившиеся поля сразу, а не падает на первомD)Ничего: assertAll — устаревший синоним последовательных ассертов
показать ответ и разбор
+C)assertAll сообщает про ВСЕ провалившиеся поля сразу, а не падает на первом// разбор: Последовательные assertEquals падают на первом же несовпадении — остальные поля остаются непроверенными, и починив одно, узнаёшь про следующее только на новом прогоне. assertAll выполняет все проверки и репортит все провалы разом — полная картина за один запуск. Уместен, когда ассерты независимы и относятся к одному объекту.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.