Статьи

Как заменить правила в JUnit 5

Недавно опубликованный альфа-релиз JUnit 5 (он же JUnit Lambda) заинтересовал меня, и, просматривая документацию, я заметил, что правила исчезли, а также бегуны и правила классов. Согласно документации, эти частично конкурирующие концепции были заменены единой последовательной моделью расширения .

За прошедшие годы мы с Фрэнком написали несколько правил, которые помогают выполнять повторяющиеся задачи, такие как тестирование пользовательского интерфейса SWT , игнорирование тестов в определенных средах , регистрация (тестирование) служб OSGi , запуск тестов в отдельных потоках и некоторые другие.

Поэтому меня особенно интересовало, что нужно сделать, чтобы преобразовать существующие правила в новую концепцию, чтобы они могли работать на JUnit 5. Для изучения возможностей расширений я выбрал два правила с совершенно разными характеристиками и попытался перенести их в JUnit 5. ,

Цель этих экспериментов — увидеть, какие концепции изменились между правилами и расширениями. Поэтому я решил переписать средства JUnit 4 без обратной совместимости.

Если вы заинтересованы в переходе с JUnit 4 на 5 или изучаете возможности запуска существующих правил в JUnit 5, вы можете присоединиться к соответствующим обсуждениям.

Первым кандидатом является ConditionalIgnoreRule, который работает в тандеме с аннотацией @ConditionalIgnore. Правило оценивает условие, которое необходимо указать с помощью аннотации, и на его основе решает, выполнен тест или нет.

Другим кандидатом является встроенное правило TemporaryFolder . Как следует из названия, он позволяет создавать файлы и папки, которые удаляются по окончании теста.

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

Расширения объяснены

Прежде чем вдаваться в детали правил перехода на расширения, давайте кратко рассмотрим новую концепцию.

junit5-расширение точка-типа иерархии

Выполнение теста следует определенному жизненному циклу. И каждая фаза этого жизненного цикла, которая может быть расширена, представлена ​​интерфейсом. Расширения могут проявлять интерес к определенным этапам в том, что они реализуют соответствующий интерфейс (ы).

С ExtendWith аннотации ExtendWith тестовый метод или класс могут выражать, что им требуется определенное расширение во время выполнения. Все расширения имеют общий суперинтерфейс: ExtensionPoint . Иерархия типов ExtensionPoint перечисляет все места, которые расширение может подключить в настоящее время.

Например, приведенный ниже код применяет вымышленный MockitoExtension который внедряет фиктивные объекты:

1
2
3
4
5
@ExtendWith(MockitoExtension.class)
class MockTest {
  @Mock
  Foo fooMock; // initialized by extension with mock( Foo.class )
}

MockitoExtension предоставит конструктор по умолчанию, чтобы его можно было создать во время выполнения и реализовать необходимый интерфейс (ы) расширения, чтобы иметь возможность внедрять @Mock во все аннотированные поля @Mock .

Условное игнорирование расширения правила

Повторяющимся шаблоном для правил является предоставление услуги в тандеме с аннотацией, которая используется для маркировки и / или настройки методов тестирования, которые хотят использовать службу. Здесь ConditionalIgnoreRule проверяет все методы тестирования, с которыми он работает, и ищет аннотацию ConditinalIgnore. Если такая аннотация найдена, ее состояние оценивается и, если выполнено, тест игнорируется.

Вот как может выглядеть ConditionalIgnoreRule в действии:

1
2
3
4
5
6
7
8
@Rule
public ConditionalIgnoreRule rule = new ConditionalIgnoreRule();
  
@Test
@ConditionalIgnore( condition = IsWindowsPlatform.class )
public void testSomethingPlatformSpecific() {
  // ...
}

А теперь давайте посмотрим, как должен выглядеть код в JUnit 5:

1
2
3
4
5
@Test
@DisabledWhen( IsWindowsPlatform.class )
void testSomethingPlatformSpecific() {
  // ...
}

Сначала вы заметите, что аннотация сменила название. Чтобы соответствовать соглашениям JUnit 5, которые используют термин отключенный вместо игнорируемого, расширение также изменило свое имя на DisabledWhen .

Хотя аннотация DisabledWhen управляется DisabledWhenExtension, не видно ничего, что объявляло бы о необходимости расширения. Причина этого называется мета-аннотации, и они лучше всего иллюстрируются при рассмотрении объявления DisabledWhen:

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(DisabledWhenExtension.class)
public @interface DisabledWhen {
  Class<? extends DisabledWhenCondition> value();
}

Аннотация (мета) аннотируется расширением, которое ее обрабатывает. А во время выполнения тестовый исполнитель JUnit 5 позаботится обо всем остальном. Если встречается аннотированный метод тестирования, и эта аннотация в свою очередь мета-аннотирована ExtendWith , ExtendWith соответствующее расширение и включается в жизненный цикл.

Действительно аккуратно, не так ли? Этот прием также позволяет избежать упущения при аннотировании метода тестирования без указания соответствующего правила.

За кулисами DisabledWhenExtension реализует интерфейс TestExexutionCondition . Для каждого метода теста вызывается его единственный evaluate() который должен возвращать ConditionEvaluationResult который определяет, следует ли выполнять тест.

Остальная часть кода в основном такая же, как и раньше. DisabledWhen аннотации DisabledWhen выполняется, и при ее обнаружении создается экземпляр указанного класса условия и спрашивается, должен ли тест выполняться или нет. Если выполнение отклонено, возвращается отключенный ConditionEvaluationResult и платформа действует соответствующим образом.

Расширение правила TemporaryFolder

Прежде чем превратить правило TemporaryFolder в исключение, давайте посмотрим, из чего состоит правило. Сначала правило обеспечивает и очищает временную папку во время настройки теста и демонтажа. Но он также предоставляет тесту доступ к методам создания (временных) файлов и папок в этой корневой папке.

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

01
02
03
04
05
06
07
08
09
10
@ExtendWith(TemporaryFolderExtension.class)
class InputOutputTest
  private TemporaryFolder tempFolder;
 
  @Test
  void testThatUsesTemporaryFolder() {
    F‌ile f‌ile = tempFolder.newF‌ile();
    // ...
  }
}

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

Чтобы внедрить TemporaryFolder , расширение реализует интерфейс InstancePostProcessor . Его метод postProcessTestInstance вызывается сразу после создания тестового экземпляра. В этом методе он имеет доступ к экземпляру теста через параметр TestExtensionContext и может внедрить TemporaryFolder во все соответствующие поля.

Для маловероятного события, когда класс объявляет несколько полей TemporaryFolder , каждому полю назначается новый экземпляр, и у каждого из них есть собственная корневая папка.

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

Для очистки после выполнения теста необходимо реализовать другой интерфейс расширения: AfterEachExtensionPoint . Его единственный метод afterEach вызывается после каждого теста. А реализация TemporaryFolderExtension очищает все известные экземпляры TemporaryFolder .

Теперь, когда мы находимся на одном уровне с функциями правила TemporaryFolder , появилась также новая функция для поддержки: внедрение зависимости уровня метода.
В JUnit 5 методам теперь разрешено иметь параметры.
Это означает, что наше расширение должно иметь возможность вводить не только поля, но и параметры метода типа TemporaryFolder .
Тест, который хочет создать временные файлы, может запросить инъекцию TemporaryFolder как в следующем примере:

1
2
3
4
5
6
7
8
class InputOutputTest {
  @Test
  @ExtendWith(TemporaryFolderExtension.class)
  void testThatUsesTemporaryFolder( TemporaryFolder tempFolder ) {
    F‌ile f‌ile = tempFolder.newF‌ile();
    // ...
  }
}

Благодаря реализации интерфейса MethodParameterResolver расширение может участвовать в разрешении параметров метода. Для каждого параметра тестового метода вызывается метод support supports() расширения, чтобы решить, может ли он предоставить значение для данного параметра. В случае TemporaryFolderExtension реализация проверяет, является ли тип параметра TemporaryFolder и возвращает true в этом случае. Если необходим более широкий контекст, метод supports() также предоставляется с текущим контекстом вызова метода и контекстом расширения.

Теперь, когда расширение решило поддерживать определенный параметр, его метод resolve() должен предоставить соответствующий экземпляр. Опять же, окружающие контексты предоставляются. TemporaryFolderExtension просто возвращает уникальный экземпляр TemporaryFolder который знает (временную) корневую папку и предоставляет методы для создания в ней файлов и подпапок.

Обратите внимание, что считается ошибкой объявлять параметр, который не может быть разрешен. Следовательно, если обнаружен параметр без соответствующего преобразователя, возникает исключение.

Хранение состояния в расширениях

Как вы могли заметить, TemporaryFolderExtension поддерживает свое состояние (т. Е. Список временных папок, которые он создал) в настоящее время в простом поле. Хотя тесты показали, что это работает на практике, нигде в документации не говорится, что один и тот же экземпляр используется при вызове различных расширений. Следовательно, если JUnit 5 меняет свое поведение в этой точке, состояние может быть потеряно во время этих вызовов.

Хорошей новостью является то, что JUnit 5 предоставляет средства для поддержания состояния расширений, называемых Store s. Как сказано в документации, они предоставляют методы для расширений для сохранения и извлечения данных .

API аналогичен упрощенному Map и позволяет хранить пары ключ-значение, получать значение, связанное с данным ключом, и удалять данный ключ. Ключи и значения могут быть произвольными объектами. TestExtensionContext хранилищу можно получить через TestExtensionContext который передается в качестве параметра каждому методу расширения (например, beforeEach , afterEach ). TestExtensionContext экземпляр TestExtensionContext инкапсулирует контекст, в котором выполняется текущий тест.

Например, в beforeEach значение будет храниться в контексте расширения следующим образом:

1
2
3
4
@Override
public void beforeEach( TestExtensionContext context ) {
  context.getStore().put( KEY, ... );
}

И позже может быть получен так:

1
2
3
4
5
6
@Override
public void afterEach( TestExtensionContext context ) {
  Store store = context.getStore();
  Object value = store.get( KEY );
  // use value...
}

Чтобы избежать возможных конфликтов имен, магазины могут быть созданы для определенных пространств имен. Метод context.getStore() использованный выше, получает хранилище для пространства имен по умолчанию. Чтобы получить магазин для определенного пространства имен, используйте

1
context.getStore( Namespace.of( MY, NAME, SPACE );

{ MY, NAME, SPACE } имен определяется через массив объектов, { MY, NAME, SPACE } в этом примере.

Упражнение по переработке TemporaryFolderExtension для использования Store оставлено на усмотрение читателя.

Выполнение кода

Проект настроен для использования в Eclipse с установленной поддержкой Maven. Но не должно быть сложно скомпилировать и запустить код в других IDE с поддержкой Maven.

Вполне естественно, что в этом раннем состоянии пока нет поддержки для запуска тестов JUnit 5 непосредственно в Eclipse. Поэтому, чтобы запустить все тесты, вы можете использовать запуск всех тестов с конфигурацией запуска ConsoleRunner . Если у вас возникли проблемы, пожалуйста, обратитесь к разделу « Выполнение тестов с JUnit 5 » в моем предыдущем посте о JUnit 5 для получения дополнительных советов или оставьте комментарий.

Заключение Как заменить правила в JUnit 5

В течение всего этого небольшого эксперимента у меня создалось впечатление, что расширения являются достойной и полной заменой правил и друзей в JUnit 4. И, наконец, использование новых методов — это весело и гораздо более лаконично, чем существующие возможности.

Если вы найдете вариант использования, который еще не может быть реализован с помощью расширений, я уверен, что команда JUnit 5 будет благодарна, если вы сообщите им об этом .

Но учтите, однако, что на данный момент расширения находятся в стадии разработки . API помечен как экспериментальный и может быть изменен без предварительного уведомления. Таким образом, сейчас может быть немного рано переносить ваши помощники JUnit 4 — если только вы не возражаете приспособить свой код к потенциально меняющимся API.

Если расширения JUnit 5 привлекли ваше внимание, вы можете также продолжить чтение соответствующей главы документации .

Ссылка: Как заменить правила в JUnit 5 от нашего партнера JCG Рудигера Херрмана в блоге Code Affine .