JUnit, часть 3: модели кастомизации



Изменение архитектуры - не всё, чем JUnit 5 отличается от предыдущей версии. Второе отличие касается модели кастомизации.



В этом посте поговорим, зачем это нужно в тестовом фреймворке, и про разницу между 4 и 5 версией.



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



Допустим, нужно измерить время выполнения каждого теста: запустить таймер вначале и вывести время выполнения в конце.



Для одного класса это несложно - просто добавляем методы с аннотациями @Before и @After.



А как посчитать время для всех классов? Здесь варианта два:



🔸 Вынести общий код в отдельный класс, в каждый класс-тест добавить методы Before и After. Решение рабочее, но придётся копипастить методы в каждый класс.



🔸 Внедрить логику где-то на верхнем уровне и включать/выключать её через настройки или аннотации.



Это и есть кастомизация - предусмотренные библиотекой места "встраивания" новой логики. JUnit 4 и 5 используют для этого разные механизмы. Давайте кратко их обсудим.



JUnit 4 Runner



Переопределяем жизненный цикл теста целиком. Наследуемся от интерфейса Runner или абстрактного класса, в нужных местах добавляем нужные действия. Теперь тесты запускаются не по стандартной схеме, а по той, что прописана в новом классе.



Примеры:

▫️ @RunWith(Parameterized.class) запускает параметризованные тесты

▫️ @RunWith(Suite.class) запускает наборы тестов

▫️ @RunWith(SpringJUnit4ClassRunner.class) добавляет спринговые активности до и после запуска теста

▫️ @RunWith(MockitoJUnitRunner.class) позволяет использовать заглушки



Главный минус - жизненный цикл только один, значит Runner для теста может быть только один. Не получится совместить несколько фич, например, параметризованные тесты с заглушками.



JUnit 4 Rule



Переопределяем интерфейс TestRule и задаём действие до и после выполнения теста. В тестах выглядит как просто поле:



@Rule

public Timeout globalTimeout = Timeout.seconds(10);



В JUnit 4 есть несколько готовых правил:

▪️ TemporaryFolder - создать временную папку для теста

▪️ ExternalResource - открыть и закрыть внешний ресурс(файл, сокет, БД)



Плюсы-минусы:



Можно использовать несколько rule в одном классе

Работает в рамках одного метода и по сути похож на before/after.



JUnit 5 Extension



Жизненный цикл теста разбивается на 10+ фаз. К каждой из них можно присоединиться, если переопределить нужный интерфейс:

▫️BeforeAllCallback - действие перед всеми тестами

▫️ParameterResolver - передача параметров в тест



Реализуем нужные интерфейсы, регистрируем класс и готово. Похожий механизм используется в Spring.



Класс может использовать несколько экстеншенов

Можно вклиниться на любых этапах жизненного цикла

В интерфейсах доступен контекст выполнения и вся информация про тесты, в итоге возможностей гораздо больше



В JUnit 5 полностью убрали поддержку Runner и Rule, всё переписано на Extension API. Кодовые базы стали несовместимы между собой, поэтому и нужна библиотека Vintage с адаптерами.

______



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



JUnit - опенсорсный проект, где никто никому не платил за работу.



Но рефакторинг назревал много лет. Однажды ребята решили, что такие грандиозные планы требуют фулл тайм и объявили краудфандинг на JUnit 5.



Сумма требовалась небольшая - 25 тысяч евро, меньше двух миллионов рублей. В итоге собрали в 2 раза больше, и уже через 6 недель был готов первый прототип.



Меня это очень впечатляет, особенно в сравнении со стоимостью и скоростью разработки в энтерпрайзе🙈