Статьи

JUnit в двух словах: тестовые бегуны

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

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

Архитектура бегунов

Не бойтесь отказаться от хорошего, чтобы пойти на великое.
Джон Д. Рокфеллер

В предыдущих статьях мы научились использовать некоторые шаблоны тестирования xUnit [MES] с JUnit. Эти концепции хорошо поддерживаются поведением инструмента по умолчанию. Но иногда возникает необходимость изменить или дополнить последний для конкретных типов испытаний или целей.

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

JUnit поддерживает использование различных типов тестовых процессоров для этой цели. Таким образом, он делегирует во время выполнения создание экземпляра класса теста, выполнение теста и создание отчетов о результатах таким процессорам, которые должны быть org.junit.Runner .

В тестовом примере можно указать ожидаемый тип бегуна с @RunWith аннотации @RunWith . Если тип не указан, среда выполнения по умолчанию выбирает BlockJUnit4ClassRunner . Который отвечает за то, что каждый тест выполняется со свежим экземпляром теста и вызывает методы жизненного цикла, такие как неявные установки или обработчики разрыва (см. Также главу о структуре теста )

1
2
@RunWith( FooRunner.class )
public class BarTest {

Фрагмент кода показывает, как мнимый FooRunner указан как тестовый процессор для мнимого BarTest .

Обычно нет необходимости писать собственные тестовые прогоны. Но в случае необходимости Майкл Шархаг недавно написал хорошее объяснение архитектуры бегунов JUnit .

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

Люкс и Категории

Вероятно, одним из самых известных процессоров является Suite . Это позволяет запускать наборы тестов и / или других наборов в иерархически или тематически структурированном виде. Обратите внимание, что сам по себе указанный класс обычно не имеет реализации тела. Он снабжен списком тестовых классов, которые выполняются при запуске пакета:

1
2
3
4
5
6
@RunWith(Suite.class)
@SuiteClasses( {
  NumberRangeCounterTest.class,
  // list of test cases and other suites
} )
public class AllUnitTests {}

Однако возможности структурирования пакетов несколько ограничены. Из-за этого JUnit 4.8 ввел менее известную концепцию Categories . Это позволяет определять пользовательские типы категорий, такие как, например, единичные, интеграционные и приемочные тесты. Чтобы назначить тестовый пример или метод одной из этих категорий, предоставляется аннотация Category :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
// definition of the available categories
public interface Unit {}
public interface Integration {}
public interface Acceptance {}
 
// category assignment of a test case
@Category(Unit.class)
public class NumberRangeCounterTest {
  [...]
}
 
// suite definition that runs tests
// of the category 'Unit' only
@RunWith(Categories.class)
@IncludeCategory(Unit.class)
@SuiteClasses( {
  NumberRangeCounterTest.class,
  // list of test cases and other suites
} )
public class AllUnitTests {}

С помощью аннотированных классов Categories определяются наборы, которые запускают только те тесты из списка классов, которые соответствуют указанным категориям. Спецификация выполняется с помощью аннотаций include и / или exclude. Обратите внимание, что категории могут использоваться в сборках Maven или Gradle без определения определенных классов набора (см. Раздел « Категории » документации JUnit ).

Для получения дополнительной информации о категориях: Джон Фергюсон Смартс написал подробное объяснение о группировании тестов с использованием категорий JUnit .

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

Но эта фильтрация не поддерживается самой JUnit, поэтому вам может понадобиться специальный бегун, который динамически собирает доступные тесты соответствия. Библиотека, которая обеспечивает соответствующую реализацию, является ClasspathSuite Йоханнеса Линка . Если вам BundleTestSuite работать с интеграционными тестами в среде OSGi, BundleTestSuite делает нечто подобное для пакетов.

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

Параметризованные тесты

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

Теперь предположим, что наш NumberRangeCounter , который инициализируется параметрами конструктора, должен быть предоставлен как API. Поэтому мы можем считать разумным, что создание экземпляра проверяет правильность заданных параметров.

Мы могли бы указать соответствующие угловые случаи, которые должны быть подтверждены с помощью IllegalArgumentException , по одному тесту каждый. Используя подход « Чистые JUnit Throwable-Tests с Java 8 Lambdas» , такой тест, проверяющий, что параметр хранилища не должен быть нулевым, может выглядеть следующим образом:

1
2
3
4
5
6
7
8
@Test
  public void testConstructorWithNullAsStorage() {
    Throwable actual = thrown( () -> new NumberRangeCounter( null, 0, 0 ) );
     
    assertTrue( actual instanceof IllegalArgumentException );
    assertEquals( NumberRangeCounter.ERR_PARAM_STORAGE_MISSING,
                  actual.getMessage() );
  }

Обратите внимание, что для проверки я использую встроенную функциональность JUnit. Я расскажу о плюсах и минусах конкретных библиотек соответствия ( Hamcrest , AssertJ ) в отдельном посте.

Чтобы сохранить этот пост в объеме, я также пропускаю дискуссию о том, будет ли NPE лучше, чем IAE.

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

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

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

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
27
28
29
30
31
32
33
34
35
36
37
38
@RunWith( Parameterized.class )
public class NumberRangeCounterTest {
   
  private final String message;
  private final CounterStorage storage;
  private final int lowerBound;
  private final int range;
   
  @Parameters
  public static Collection<Object[]> data() {
    CounterStorage dummy = mock( CounterStorage.class );
    return Arrays.asList( new Object[][] {
      { NumberRangeCounter.ERR_PARAM_STORAGE_MISSING, null, 0, 0 },
      { NumberRangeCounter.ERR_LOWER_BOUND_NEGATIVE, dummy, -1, 0 },
       [...] // further data goes here...
    } );
  }
   
  public NumberRangeCounterTest(
    String message, CounterStorage storage, int lowerBound, int range )
  {
    this.message = message;
    this.storage = storage;
    this.lowerBound = lowerBound;
    this.range = range;
  }
   
  @Test
  public void testConstructorParamValidation() {
    Throwable actual = thrown( () ->
      new NumberRangeCounter( storage, lowerBound, range ) );
     
    assertTrue( actual instanceof IllegalArgumentException );
    assertEquals( message, actual.getMessage() );
  }
 
  [...]
}

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

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

JUnitParams

Библиотека JUnitParams предоставляет типы JUnitParamsRunner и @Parameter . Аннотация к параметру указывает записи данных для данного теста. Обратите внимание на разницу с аннотацией JUnit с таким же простым именем. Последний отмечает метод, который обеспечивает записи данных!

Приведенный выше тестовый сценарий можно переписать с помощью JUnitParams, как показано в следующем фрагменте:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RunWith( JUnitParamsRunner.class )
public class NumberRangeCounterTest {
   
  public static Object data() {
    CounterStorage dummy = mock( CounterStorage.class );
    return $( $( ERR_PARAM_STORAGE_MISSING, null, 0, 0 ),
              $( ERR_LOWER_BOUND_NEGATIVE, dummy, -1, 0 ) ); 
  }
   
  @Test
  @Parameters( method = "data" )
  public void testConstructorParamValidation(
    String message, CounterStorage storage, int lowerBound, int range )
  {
    Throwable actual = thrown( () ->
      new NumberRangeCounter( storage, lowerBound, range ) );
     
    assertTrue( actual instanceof IllegalArgumentException );
    assertEquals( message, actual.getMessage() );
  }
   
  [...]
}

Хотя это, безусловно, более компактно и на первый взгляд выглядит чище, некоторые конструкции нуждаются в дальнейшем объяснении. Метод $(...) определен в JUnitParamsRunner (статический импорт) и является ярлыком для создания массивов объектов. Привыкнув к этому, определение данных становится более читабельным.

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

Сам тест имеет дополнительную аннотацию @Parameters . Объявление метода аннотации относится к поставщику данных, который используется для предоставления тесту объявленных параметров. Имя метода разрешается во время выполнения с помощью отражения. Это обратная сторона решения, так как оно небезопасно во время компиляции.

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

Другим огромным преимуществом является то, что теперь только те тесты работают с записями данных, которые используют аннотацию @Parameters . Стандартные тесты выполняются только один раз. Это, в свою очередь, означает, что параметризованные тесты могут храниться в стандартном тестовом примере устройства.

JUnit-PARAMS-тест

Заворачивать

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

Для получения списка дополнительных участников тестирования, страницы Test Runners и Custom Runners на junit.org могут быть хорошей отправной точкой. И если вы задаетесь вопросом, о чем рассказывает автор « Theories в заглавной картинке, вы можете взглянуть на пост Флориана Вайбелса «Юнит» — «Разница между практикой и @Theory» .

В следующий раз о JUnit в двух словах я наконец расскажу о различных типах утверждений, доступных для проверки результатов тестирования.

использованная литература

[MES] Тестовые таблицы xUnit, Джерард Месарос, 2007

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