В первый раз, когда я наткнулся на аннотацию JUnit @Rule я был немного раздражен этой концепцией. Наличие публичного поля в тестовом примере показалось мне странным, и я неохотно использовал его регулярно. Но через некоторое время я привык к этому, и оказалось, что правила могут облегчить написание тестов разными способами. Этот пост дает краткое представление о концепции и некоторые короткие примеры того, для чего хороши правила.
Каковы правила JUnit?
Давайте начнем с рассмотрения стандартного правила JUnit. TemporaryFolder — это помощник по тестированию, который можно использовать для создания файлов и папок, расположенных в каталоге файловой системы для временного содержимого 1 . Интересной особенностью TemporaryFolder является то, что он гарантирует удаление своих файлов и папок после завершения метода тестирования 2 . Чтобы работать должным образом, экземпляр временной папки должен быть назначен аннотированному полю @Rule которое должно быть открытым, а не статическим, и подтипом TestRule :
|
01
02
03
04
05
06
07
08
09
10
|
public class MyTest { @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); @Test public void testRun() throws IOException { assertTrue( temporaryFolder.newFolder().exists() ); }} |
Как это работает?
Правила предоставляют возможность перехватывать вызовы тестовых методов, аналогично тому, как это делает инфраструктура AOP . В сравнении с советом в AspectJ вы можете делать полезные вещи до и / или после фактического выполнения теста 3 . Хотя это звучит сложно, это довольно легко достичь.
Часть API определения правила должна реализовывать TestRule. Единственный метод этого интерфейса, называемый apply возвращает Statement . Statement S представляют собой — просто произносимые — ваши тесты во время выполнения JUnit, а Statement#evaluate() выполняет их. Теперь основная идея состоит в том, чтобы предоставить расширения-обертки Statement которые могут вносить реальный вклад, переопределяя Statement#evaluate() :
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public class MyRule implements TestRule { @Override public Statement apply( Statement base, Description description ) { return new MyStatement( base ); }}public class MyStatement extends Statement { private final Statement base; public MyStatement( Statement base ) { this.base = base; } @Override public void evaluate() throws Throwable { System.out.println( 'before' ); try { base.evaluate(); } finally { System.out.println( 'after' ); } }} |
MyStatement реализован как оболочка, которая используется в MyRule#apply(Statement,Destination) для переноса исходного оператора, заданного в качестве аргумента. Легко видеть, что оболочка переопределяет Statement#evaluate() для выполнения чего-либо до и после фактической оценки теста 4 .
Следующий фрагмент показывает, как MyRule может использоваться точно так же, как и TemporaryFolder выше:
|
01
02
03
04
05
06
07
08
09
10
|
public class MyTest { @Rule public MyRule myRule = new MyRule(); @Test public void testRun() { System.out.println( 'during' ); }} |
Запуск тестового примера приводит к следующему выводу консоли, который доказывает, что наше примерное правило работает должным образом. Выполнение теста перехватывается и модифицируется нашим правилом для печати «до» и «после» вокруг «во время» теста:
|
1
2
3
|
beforeduringafter |
Теперь, когда основы понятны, давайте посмотрим на несколько более полезных вещей, которые вы можете сделать с помощью правил.
Испытательные приспособления
В соответствующем разделе Википедии процитировано тестовое приспособление — это все, что должно быть на месте, чтобы выполнить тест и ожидать определенного результата. Часто фикстуры создаются путем обработки событий setUp() и tearDown() платформы модульного тестирования ».
С JUnit это часто выглядит примерно так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public class MyTest { private MyFixture myFixture; @Test public void testRun1() { myFixture.configure1(); // do some testing here } @Test public void testRun2() { myFixture.configure2(); // do some testing here } @Before public void setUp() { myFixture = new MyFixture(); } @After public void tearDown() { myFixture.dispose(); }} |
Представьте, что вы используете определенный прибор, как показано выше во многих ваших тестах. В этом случае было бы неплохо избавиться от setUp() и tearDown() . Учитывая вышеприведенные разделы, мы теперь знаем, что это можно сделать, изменив MyFixture для реализации TestRule . Соответствующая реализация Statement должна гарантировать, что она вызывает MyFixture#dispose() и может выглядеть следующим образом:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
public class MyFixtureStatement extends Statement { private final Statement base; private final MyFixture fixture; public MyFixtureStatement( Statement base, MyFixture fixture ) { this.base = base; this.fixture = fixture; } @Override public void evaluate() throws Throwable { try { base.evaluate(); } finally { fixture.dispose(); } }} |
С этим на месте тест выше может быть переписан как:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public class MyTest { @Rule public MyFixture myFixture = new MyFixture(); @Test public void testRun1() { myFixture.configure1(); // do some testing here } @Test public void testRun2() { myFixture.configure2(); // do some testing here }} |
Я ценю более компактную форму написания тестов с использованием правил во многих случаях, но, безусловно, это также вопрос вкуса и того, что вы считаете лучше читать 5 .
Конфигурация прибора с аннотациями методов
До сих пор я молча игнорировал аргумент Description TestRule#apply(Statement,Description) . В общем, Description описывает тест, который должен быть запущен или уже был запущен. Но это также позволяет получить доступ к некоторой отражающей информации о базовом методе Java. Среди прочего можно прочитать аннотации, прилагаемые к такому методу. Это позволяет нам комбинировать правила с аннотациями методов для удобства настройки TestRule .
Рассмотрим этот тип аннотации:
|
1
2
3
4
5
|
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface Configuration { String value();} |
В сочетании со следующим фрагментом внутри MyFixture#apply(Statement,Destination) который считывает значение конфигурации, аннотированное для определенного метода тестирования…
|
1
2
3
4
|
Configuration annotation = description.getAnnotation( Configuration.class );String value = annotation.value();// do something useful with value |
… MyFixture выше тестовый пример, демонстрирующий использование правила MyFixture можно переписать так:
|
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public class MyTest { @Rule public MyFixture myFixture = new MyFixture(); @Test @Configuration( value = 'configuration1' ) public void testRun1() { // do some testing here } @Test @Configuration( value = 'configuration2' ) public void testRun2() { // do some testing here }} |
Конечно, у последнего подхода есть ограничения, связанные с тем, что аннотации допускают только литералы Enum , Class или String качестве параметров. Но есть случаи, когда этого вполне достаточно. Хороший пример использования правил в сочетании с аннотациями методов представлен библиотекой restfuse . Если вас интересует пример из реального мира, вам следует взглянуть на реализацию библиотекой правила Destination 6 .
В завершение осталось сказать лишь то, что я хотел бы услышать от вас другие полезные примеры правил JUnit, которые вы могли бы использовать для облегчения своей ежедневной работы по тестированию:
- Каталог, который обычно возвращается
System.getProperty( 'java.io.tmpdir' );↩ - Глядя на реализацию
TemporaryFolderя должен отметить, что он не проверяет, было ли удаление файла успешным. Это может быть слабым местом в случае использования открытых файлов ↩ - И за то, что это стоит, вы даже можете заменить полный метод теста на что-то другое ↩
- Делегирование обернутого оператора помещается в блок
try...finallyчтобы гарантировать выполнение функциональности после выполнения теста, даже если тест завершится неудачно. В этом случае будетAssertionErrorи все операторы, которые не находятся в блоке finally, будут пропущены ↩ - Вы, вероятно, заметили, что пример
TemporaryFolderв начале — это не что иное, как сценарий использования прибора. - Обратите внимание, что класс
DestinationMethodRuleреализуетMethodRuleвместоTestRule. Этот пост основан на последней версии JUnit, гдеMethodRuleбыл помечен как@Deprecated.TestRuleявляется заменой дляMethodRule. Но, учитывая знание этого поста, все же должно быть легко понять реализацию ↩
Ссылка: Правила JUnit от нашего партнера JCG Фрэнка Аппеля в блоге Code Affine .