JUnit 5 разрабатывался около 14 месяцев, а прототипу почти год. Пришло время подвести итог тому, что произошло до сих пор, где находится проект и куда он идет.
Все технические данные основаны на текущей версии JUnit, этап 2 . Более подробную информацию вы найдете в официальном руководстве пользователя, с отдельными версиями milestone 2 и самой последней сборкой , или в моем блоге , где я написал серию о JUnit 5 , которую я обновляю всякий раз, когда выпускается новая веха.
Ранее в Battlestar JUnit
Почему Юнит 5? Почему не 4.13, который был в разработке с тех пор, вечно? Две с половиной причины …
До свидания, Юнит 4
Частично успех JUnit связан с его великолепной поддержкой инструментов, для которой создатели инструментов даже использовали рефлексию (вплоть до частных полей) для доступа к информации, которую API не раздает. Это связывало инструменты с деталями реализации, что, в свою очередь, мешало разработчикам JUnit развивать его — никому не нравятся взломанные проекты. Это важная причина того, почему прогресс JUnit в основном остановился.
Тогда есть бегуны и правила . Они были созданы, чтобы расширить JUnit и сделали хорошую работу в этом — хорошо, но не превосходно. Бегуны — это тяжеловесная концепция, в которой нужно управлять всем жизненным циклом испытаний. Но что еще хуже, вы можете использовать только одного бегуна за раз — очень строгое ограничение. Вот почему правила были введены в 4.7. Несмотря на то, что они намного проще и в основном сочетаемы, у них есть и другие недостатки, а именно то, что они ограничены определенными типами поведения, которые лучше всего суммировать, как до / после.
Последней и наименее была версия Java. В наши дни все хотят поиграть с лямбдами, и если бы JUnit 4 требовал Java 8, это было бы трудно продать.
Здравствуйте, JUnit 5
Так что в 2015 году команда сформировалась вокруг идеи полного переписывания. Вначале получивший название JUnit Lambda , проект провел успешную краудфандинговую кампанию и получил достаточно денег и спонсорскую поддержку работодателя, чтобы работать над ним в течение шести месяцев. Затем последовали семинары, прототип, альфа-версия и два этапа, а с начала этого года проект называется JUnit 5 . К настоящему времени средства израсходованы, и они разрабатываются, как и многие другие замечательные проекты с открытым исходным кодом: преданными своему делу людьми в свободное время в течение первого и последнего часов дня.
В настоящее время команда завершает третий этап и планирует еще как минимум два, прежде чем кандидат на освобождение будет рассмотрен. Но мы вернемся к этому. Давайте сначала посмотрим, что у нас в руках на данный момент.
Как это выглядит?
Вместо того, чтобы вдаваться в подробности о новом API, я прошу вас представить полномасштабные тесты JUnit 4 со всеми махинациями, которые предлагает API. Вот как JUnit 5 отличается от этого:
- Тестовые классы и методы могут быть видны в пакете.
- Методы интерфейса по умолчанию могут быть методами тестирования.
-
@Test
больше не принимает параметры. Ожидаемые исключения и тайм-ауты реализуются через утверждения. - Вы можете создавать тесты динамически во время выполнения с
@TestFactory
аннотированных методов@TestFactory
(подробнее об этом позже). - Аннотации жизненного цикла получили новые названия:
@Before
стал@BeforeEach
а@BeforeClass
теперь@BeforeAll
; аналогично для@After...
-
@Ignore
изменено на@Disabled
. - В утверждениях сообщения об ошибках идут последними и могут создаваться лениво.
- Новое утверждение
assertAll
выполняет данные проверки и всегда сообщает обо всех результатах, даже если некоторые утверждения не выполнены. -
@Nested
можно использовать для запуска тестов во внутренних классах. -
@DisplayName
присваивает@DisplayName
удобочитаемые имена. - Пропуски и правила исчезли и заменены новым механизмом расширения, который легко интегрируется с нативным API (подробнее об этом позже).
- Тесты могут иметь параметры, где экземпляры создаются расширениями (опять же, позже).
В общем, вот как может выглядеть тестовый класс:
@DisplayName("Some example test cases") class ExampleTests { @BeforeAll static void initAll() { } @BeforeEach void init() { } @Test @DisplayName("Custom test name containing spaces") void succeedingTest() { } @Test void groupedAssertions() { // In a grouped assertion all assertions are executed, and any // failures will be reported together. assertAll("address", () -> assertEquals("John", address.getFirstName()), () -> assertEquals("User", address.getLastName()) ); } @Test @DisplayName("") @Disabled("for demonstration purposes") void skippedTest() { // not executed } @Test @CoolInstanceProvider void testWithParameters(MyCoolClass coolInstance) { // do something with 'coolInstance' } @TestFactory Collection<DynamicTest> dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("1st dynamic test", () -> assertTrue(true)), dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2)) ); } @AfterEach void tearDown() { } @AfterAll static void tearDownAll() { } @DisplayName("Some nested test cases") class NestedTest { @Test @DisplayName("╯°□°)╯") void failingTest() { fail("a failing test"); } } }
Что касается @Nested
и @DisplayName
, то вот как выглядит пример, написанный командой JUnit в IntelliJ 2016.2:
Неплохо, правда? Для более детального ознакомления с руководством пользователя или моим постом, охватывающим основы .
Как я могу использовать это?
Чтобы начать писать тесты, все что вам нужно сделать, это включить артефакт API
org.junit.jupiter:junit-jupiter-api:5.0.0-M2
в вашем проекте. К сожалению, выполнение тестов немного сложнее …
Иды
Встроенная поддержка IDE необходима для любой серьезной среды тестирования. JUnit 5 еще не выпущен, но это понятно, учитывая, что он даже официально не выпущен.
- IntelliJ , начиная с 2016 года , имеет встроенную поддержку, которая работает достаточно хорошо.
- Команда Eclipse начала работать над этим — посмотрите соответствующие проблемы в JUnit и в Eclipse .
- NetBeans не имеет поддержки, и я даже не смог найти проблему .
Инструменты сборки
Как насчет поддержки встроенных инструментов? Команда JUnit внедрила провайдера Maven Surefire и плагин Gradle, но это всего лишь подтверждение концепции. Чтобы полностью интегрировать и протестировать их, команда хотела бы, чтобы соответствующие проекты взяли на себя эти кодовые базы.
- Для Maven этот процесс только начался с повторного лицензирования исходного кода под лицензией Apache . Проблема в Apache JIRA отслеживает прогресс.
- Я не видел никаких связанных с Gradle действий в проекте JUnit и не смог найти проблему, связанную с JUnit 5.
откаты
Эта ситуация немного неудовлетворительная, но, к счастью, есть альтернативы. Один из них — просто обернуть все тесты JUnit 5 в набор тестов JUnit 4 и рассчитывать на поддержку JUnit 4:
@RunWith(JUnitPlatform.class) @SelectPackages({ "my.test.package" }) public class JUnit5TestSuite { }
См. Руководство пользователя для деталей настройки.
Другой альтернативой является консоль запуска. Опять же, обратитесь к руководству пользователя, чтобы начать, и ознакомьтесь с моим постом установки JUnit, если у вас возникнут проблемы. Когда вы закончите, вы можете запустить тесты следующим образом:
junit-platform-console -p -p target/classes/:target/test-classes -a
Космическое пространство
Теперь, когда вы знаете, как писать и запускать тесты JUnit 5, давайте пойдем дальше основ и взглянем на дополнительные темы.
Динамические тесты
Динамические тесты — это новая концепция в JUnit 5. Они позволяют легко создавать тесты во время выполнения, которые инструменты будут распознавать как полные тестовые случаи.
Пример объясняет это лучше всего. Предположим, у вас есть метод testPoint(Point p)
который утверждает что-то в заданной точке, и вы хотите запустить этот тест для множества точек. Вы можете написать простой цикл:
@Test void testPoints() { getTestPoints().forEach(this::testPoint); }
Это имеет существенный недостаток: хотя концептуально это много тестов, для JUnit это всего лишь один. Первым среди нежелательных последствий является то, что, как только первый тест не проходит, все остальные даже не выполняются. Другое — инструменты, в частности IDE, сообщат об этом как об одном тесте, который тоже не идеален.
JUnit 5 дает нам выход. Тестовый код может быть DynamicTest
экземпляр DynamicTest
который создается во время выполнения. Методы, которые их возвращают, могут быть аннотированы новой аннотацией @TestFactory
и JUnit будет вызывать их, вставлять созданные тесты в план тестирования и выполнять их.
@TestFactory Stream<DynamicText> testingPoints() { return getTestPoints() .stream() .map(p -> dynamicTest("Testing " + p, () -> pointTest(p))); }
Ухоженная. И если вы действительно хотите, вы можете использовать динамические тесты для написания тестов с лямбда-выражениями
Модель расширения
Я уже упоминал о бегунах и правилах JUnit 4. Они исчезли, их заменили точки расширения, с которыми может взаимодействовать сторонний код. Вот как это работает.
Точки расширения
Для большинства этапов жизненного цикла теста существует точка расширения:
- Постпроцессор тестового экземпляра
- BeforeAll Обратный звонок
- Тест и условие выполнения контейнера
- BeforeEach Callback
- Разрешение параметра
- Перед выполнением теста
- После выполнения теста
- Обработка исключений
- AfterEach Callback
- AfterAll Callback
Для каждой из этих точек расширения существует интерфейс, и, поскольку точки очень сфокусированы, интерфейсы довольно просты. Вот тот, для обратного вызова «Перед каждым»:
public interface BeforeEachCallback extends Extension { void beforeEach(TestExtensionContext context) throws Exception; }
Я уже отмечал, что тесты могут иметь параметры. Поскольку JUnit не знает, как создать экземпляр MyCoolClass
, расширения должны предоставить их. Вот эта точка расширения:
public interface ParameterResolver extends Extension { boolean supports( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException; Object resolve( ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException; }
Для каждого параметра JUnit будет вызывать support
для каждого зарегистрированного расширения, генерировать исключение, если не одно из них возвращает true
, и в противном случае запрашивать одно расширение для разрешения параметра.
В качестве подтверждения концепции команда написала расширение Mockito, которое @Mock
параметры, помеченные @Mock
. Может использоваться следующим образом:
void myCoolClassTest(@Mock MyCoolClass instance) { // ... }
Написание расширений
Чтобы создать расширение, вы просто реализуете интерфейсы, соответствующие точкам расширения, с которыми хотите взаимодействовать, и регистрируете свой класс в JUnit. Во время выполнения теста JUnit будет приостанавливать работу в каждой точке расширения, собирать всю соответствующую информацию в экземпляр ExtensionContext
и вызывать все расширения, зарегистрированные в этой точке.
Контекст расширения предоставляет доступ ко многим интересным вещам, например, к тестовому классу или методу в виде AnnotatedElement
, поэтому расширение может отражать их. Тогда есть ExtensionStore
. Поскольку JUnit не заинтересован в отслеживании экземпляров расширений, он не дает никаких гарантий относительно их жизненного цикла. Следовательно, классы расширения должны быть без сохранения состояния и могут использовать хранилище для сохранения информации. Другим интересным аспектом является TestReporter
, который позволяет расширениям (и тестам в этом отношении) публиковать сообщения таким образом, чтобы их можно было легко найти с помощью внешних инструментов.
В совокупности тестовое расширение, которое просто сообщает о времени выполнения теста, выглядит следующим образом:
class SimpleBenchmarkExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { // we need a namespace to access the store and a key to persist information private static final Namespace NAMESPACE = Namespace .create("com", "sitepoint", "SimpleBenchmarkExtension"); private static final String LAUNCH_TIME_KEY = "LaunchTime"; @Override public void beforeTestExecution(TestExtensionContext context) { storeNowAsLaunchTime(context); } @Override public void afterTestExecution(TestExtensionContext context) { long launchTime = loadLaunchTime(context); long elapsedTime = currentTimeMillis() - launchTime; report(context, elapsedTime); } private static void storeNowAsLaunchTime(ExtensionContext context) { context.getStore(NAMESPACE).put(LAUNCH_TIME_KEY, currentTimeMillis()); } private static long loadLaunchTime(ExtensionContext context) { return context.getStore(NAMESPACE).get(LAUNCH_TIME_KEY, long.class); } private static void report(ExtensionContext context, long elapsedTime) { String message = String .format("Test '%s' took %d ms.", context.getDisplayName(), elapsedTime); // 'publishReportEntry' pipes key-value pairs into the test reporter context.publishReportEntry(singletonMap("Benchmark", message)); } }
Регистрация расширений
Я глянул на то, как можно зарегистрировать расширения. Но очень клевая особенность скрывается здесь! Прочтите мой пост о расширениях, чтобы узнать, как вы можете заставить его выглядеть следующим образом:
@Benchmark @Test void test() { // ... }
Архитектура
Быстрое погружение в архитектуру, что действительно интересно. Чтобы разделить заботу тестировщиков, разработчиков расширений и инструментов, а также позволить другим инфраструктурам тестирования воспользоваться преимуществами великолепной поддержки инструментов JUnit (ну, пока она еще не готова, но будет), JUnit 5 разделен на три подпроекта:
- JUnit Jupiter предоставляет API, о котором мы говорили ранее, и механизм, который обнаруживает и запускает тесты.
- JUnit Vintage адаптирует JUnit 3/4 для работы в новой архитектуре JUnit 5, что означает, что он также предоставляет движок.
- Платформа JUnit — это место, где происходит волшебство. Он предоставляет API для инструментов, чтобы инструктировать выполнение теста, и когда он вызывается, он находит механизмы тестирования (как два вышеупомянутых), передает инструкцию им и позволяет им делать свое дело.
С одной стороны, это облегчает запуск тестов JUnit 3, 4 и 5 в рамках одного механизма. Но разделение между платформой, которая содержит API движка, и реализациями движка также позволяет интегрировать все виды тестовых сред в механизм JUnit. Предполагая, что механизм TestNG будет существовать, вот как это получится:
Это большое дело! Создание новой среды тестирования — непростая битва, потому что принятие зависит от поддержки инструментов, а поддержка инструментов зависит от принятия. Теперь рамки могут вырваться из этого порочного круга! Они могут реализовать движок JUnit и получить полную поддержку во многих инструментах с первого дня.
Это может привести к новому поколению платформ тестирования!
Дорога отсюда
JUnit находится в очень хорошей форме и уже может быть использован для замены ванильного JUnit 4. Но, в соответствии с принципом Парето, еще предстоит проделать определенную работу, прежде чем он будет готов выполнить все варианты использования.
Текущие обсуждения
Обсуждается много очень интересных тем!
JUnit 4 Совместимость
Разве не было бы замечательно создавать адаптеры для существующих правил JUnit 4? Это значительно упростит миграцию тестов в API JUnit Jupiter (в отличие от простого запуска их в JUnit 5). Вопрос № 433 исследует именно это. Но код — не единственная хорошая вещь, которую может создать проект. Проблемы № 169 и № 292 направлены на обеспечение хорошего руководства по миграции с 4 до 5.
Модель расширения
Лично я думаю, что чего-то не хватает в динамических тестах. Было бы здорово, чтобы расширения взаимодействовали с этой функцией! Один из способов сделать это — точка расширения, в которую могут подключаться сторонние библиотеки для внедрения динамически созданных тестов. Затем они могли бы предложить интересные API для параметризации тестов, например. Обсуждение дешево. тем не менее, поэтому я создал проблему и прототипную реализацию для этой функции. Я надеюсь, что это будет рассмотрено во время разработки этапа 4.
Другая деталь заключается в том, что хотя методы фабрики тестов являются частью полного жизненного цикла, динамические тесты, которые они создают, не являются таковыми. Значение до и после обратных вызовов или разрешение параметров не работают для динамических тестов. Это отслеживается проблемой, и я уверен, что решение будет найдено до выпуска.
В настоящее время также невозможно взаимодействовать с потоковой моделью Юпитера. Это проблема для расширений, которые хотят перенести тесты в другие потоки, например, чтобы убедиться, что тесты Swing или JavaFX выполняются в потоке диспетчеризации событий или в потоке приложения, соответственно. Есть несколько проблем, связанных с этим: # 16 , # 20 , # 157
Другие интересные темы:
- точки расширения …
- глобальный реестр расширений, # 448
- растяжки на полях, № 497
- API для управления программными расширениями, # 506
Сценарные тесты
Большой темой являются тесты сценариев, которые являются номенклатурой JUnit для тестов, которые упорядочены и работают в общем состоянии (отслеживается # 48 ). TestNG предлагает это уже и является важной причиной для людей отойти от JUnit. Ну, была причина.
Допустим, у нас есть класс, инкапсулирующий REST API. С помощью тестов сценариев мы могли бы иметь первый тест, который создает экземпляр, второй тест мог бы его инициализировать, третий входил в систему пользователя и т. Д. Состояние остается от испытания к испытанию, и те изменяют его таким образом, который готовит его к следующему.
другие
Вот пара других интересных идей:
- механизм запуска для определения возможностей двигателя, # 90
- проверки работоспособности тестового кода, написанного разработчиками, # 121
- множество улучшений в консоли запуска, фильтр поиска
- более гибкая модель потоков, например, для распараллеливания тестов, поисковый фильтр
Больше вех
Каков план на следующие месяцы? Когда-нибудь в течение следующих нескольких недель будет выпущен milestone 3, который фокусируется на совместимости с JUnit 4 и улучшенном API для инструментов. После этого запланированы еще два этапа: номер 4, посвященный параметризованным тестам, расширенные динамические тесты и документация, и номер 5, посвященный тестам сценариев, повторным тестам и выполнению тестов в пользовательском потоке.
Резюме
Мы быстро ознакомились с API, с которым собираемся написать тесты, и увидели, что он похож на JUnit 4, но содержит много вдумчивых дополнений. В первую очередь это встроенная поддержка вложенных тестов, удобочитаемое отображаемое имя и динамически создаваемые тесты. Модель расширения очень перспективна, и архитектура, разделяющая JUnit 5 на Jupiter, Vintage и Platform, просто великолепна, наделяя другие тестовые среды полной поддержкой инструментов, как только они внедряют движок.
С другой стороны, мы изучили историю создания JUnit 5 и потратили некоторое время на обсуждение текущего состояния поддержки инструментов, которого, за исключением IntelliJ, по-прежнему не хватает. Наконец, мы рассмотрели некоторые из текущих дискуссий и будущих событий, которые, как мне кажется, столь же захватывающие, как и многообещающие. Я не могу дождаться выхода релиза JUnit 5 GA — надеюсь, когда-нибудь в 2017 году.