В первый раз, когда я наткнулся на аннотацию 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
|
before during after |
Теперь, когда основы понятны, давайте посмотрим на несколько более полезных вещей, которые вы можете сделать с помощью правил.
Испытательные приспособления
В соответствующем разделе Википедии процитировано тестовое приспособление — это все, что должно быть на месте, чтобы выполнить тест и ожидать определенного результата. Часто фикстуры создаются путем обработки событий 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
в начале — это не что иное, как сценарий использования прибора. - Обратите внимание, что класс
Destination
MethodRule
реализуетMethodRule
вместоTestRule
. Этот пост основан на последней версии JUnit, гдеMethodRule
был помечен как@Deprecated
.TestRule
является заменой дляMethodRule
. Но, учитывая знание этого поста, все же должно быть легко понять реализацию ↩
Ссылка: Правила JUnit от нашего партнера JCG Фрэнка Аппеля в блоге Code Affine .