Статьи

Правила JUnit

В первый раз, когда я наткнулся на аннотацию 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, которые вы могли бы использовать для облегчения своей ежедневной работы по тестированию:

  1. Каталог, который обычно возвращается System.getProperty( 'java.io.tmpdir' );
  2. Глядя на реализацию TemporaryFolder я должен отметить, что он не проверяет, было ли удаление файла успешным. Это может быть слабым местом в случае использования открытых файлов
  3. И за то, что это стоит, вы даже можете заменить полный метод теста на что-то другое
  4. Делегирование обернутого оператора помещается в блок try...finally чтобы гарантировать выполнение функциональности после выполнения теста, даже если тест завершится неудачно. В этом случае будет AssertionError и все операторы, которые не находятся в блоке finally, будут пропущены
  5. Вы, вероятно, заметили, что пример TemporaryFolder в начале — это не что иное, как сценарий использования прибора.
  6. Обратите внимание, что класс Destination MethodRule реализует MethodRule вместо TestRule . Этот пост основан на последней версии JUnit, где MethodRule был помечен как @Deprecated . TestRule является заменой для MethodRule . Но, учитывая знание этого поста, все же должно быть легко понять реализацию

Ссылка: Правила JUnit от нашего партнера JCG Фрэнка Аппеля в блоге Code Affine .