Перейти к основному содержимому

Параллельное выполнение тестов для JUnit 5

· 5 мин. чтения

1. Введение

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

Параллельное выполнение тестов — это экспериментальная функция, доступная по подписке начиная с версии 5.3.

2. Конфигурация

Во- первых, нам нужно создать файл junit-platform.properties в нашей папке src/test/resources , чтобы разрешить параллельное выполнение тестов . Мы включаем функцию распараллеливания, добавляя следующую строку в указанный файл:

junit.jupiter.execution.parallel.enabled = true

Давайте проверим нашу конфигурацию, выполнив несколько тестов. Сначала создадим класс FirstParallelUnitTest и два теста в нем:

public class FirstParallelUnitTest{

@Test
public void first() throws Exception{
System.out.println("FirstParallelUnitTest first() start => " + Thread.currentThread().getName());
Thread.sleep(500);
System.out.println("FirstParallelUnitTest first() end => " + Thread.currentThread().getName());
}

@Test
public void second() throws Exception{
System.out.println("FirstParallelUnitTest second() start => " + Thread.currentThread().getName());
Thread.sleep(500);
System.out.println("FirstParallelUnitTest second() end => " + Thread.currentThread().getName());
}
}

Когда мы запускаем наши тесты, мы получаем следующий вывод в консоли:

FirstParallelUnitTest second() start => ForkJoinPool-1-worker-19
FirstParallelUnitTest second() end => ForkJoinPool-1-worker-19
FirstParallelUnitTest first() start => ForkJoinPool-1-worker-19
FirstParallelUnitTest first() end => ForkJoinPool-1-worker-19

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

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

Динамическая стратегия определяет количество потоков на основе количества процессоров/ядер, умноженного на факторный параметр (по умолчанию 1), указанный с помощью:

junit.jupiter.execution.parallel.config.dynamic.factor

С другой стороны, фиксированная стратегия опирается на предопределенное количество потоков, указанное следующим образом:

junit.jupiter.execution.parallel.config.fixed.parallelism

Чтобы использовать пользовательскую стратегию, нам нужно сначала создать ее, реализовав интерфейс ParallelExecutionConfigurationStrategy .

3. Проверка распараллеливания внутри класса

Мы уже включили параллельное выполнение и выбрали стратегию. Теперь пришло время выполнять тесты параллельно в одном классе. Есть два способа настроить это. Один использует аннотацию @Execution(ExecutionMode.CONCURRENT) , а второй использует файл свойств и строку:

junit.jupiter.execution.parallel.mode.default = concurrent

После того, как мы выберем, как это настроить, и запустим наш класс FirstParallelUnitTest , мы увидим следующий вывод:

FirstParallelUnitTest second() start => ForkJoinPool-1-worker-5
FirstParallelUnitTest first() start => ForkJoinPool-1-worker-19
FirstParallelUnitTest second() end => ForkJoinPool-1-worker-5
FirstParallelUnitTest first() end => ForkJoinPool-1-worker-19

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

Существует также возможность запускать все тесты в классе FirstParallelUnitTest в одном потоке. В текущей области использование параллелизма и одного и того же потока нецелесообразно, поэтому давайте расширим нашу область и добавим еще один тестовый класс в следующем разделе.

4. Проверка распараллеливания внутри модуля

Прежде чем ввести новое свойство, мы создадим класс SecondParallelUnitTest с двумя методами, аналогичными FirstParallelUnitTest:

public class SecondParallelUnitTest{

@Test
public void first() throws Exception{
System.out.println("SecondParallelUnitTest first() start => " + Thread.currentThread().getName());
Thread.sleep(500);
System.out.println("SecondParallelUnitTest first() end => " + Thread.currentThread().getName());
}

@Test
public void second() throws Exception{
System.out.println("SecondParallelUnitTest second() start => " + Thread.currentThread().getName());
Thread.sleep(500);
System.out.println("SecondParallelUnitTest second() end => " + Thread.currentThread().getName());
}
}

Прежде чем мы запустим наши тесты в одном пакете, нам нужно установить свойство:

junit.jupiter.execution.parallel.mode.classes.default = concurrent

Когда мы запускаем оба тестовых класса, мы получаем следующий вывод:

SecondParallelUnitTest second() start => ForkJoinPool-1-worker-23
FirstParallelUnitTest first() start => ForkJoinPool-1-worker-19
FirstParallelUnitTest second() start => ForkJoinPool-1-worker-9
SecondParallelUnitTest first() start => ForkJoinPool-1-worker-5
FirstParallelUnitTest first() end => ForkJoinPool-1-worker-19
SecondParallelUnitTest first() end => ForkJoinPool-1-worker-5
FirstParallelUnitTest second() end => ForkJoinPool-1-worker-9
SecondParallelUnitTest second() end => ForkJoinPool-1-worker-23

Из вывода видно, что все четыре теста выполняются параллельно в разных потоках.

Комбинируя два свойства, которые мы упоминали в этом и предыдущем разделе, и их значения ( same_thread и concurrent ), мы получаем четыре различных режима выполнения:

  1. ( same_thread, same_thread ) — все тесты запускаются последовательно
  2. ( same_thread, concurrent ) — тесты из одного класса выполняются последовательно, но несколько классов выполняются параллельно
  3. ( concurrent, same_thread ) — тесты одного класса выполняются параллельно, но каждый класс выполняется отдельно
  4. ( concurrent, concurrent ) — тесты выполняются параллельно

5. Синхронизация

В идеальных ситуациях все наши модульные тесты независимы и изолированы. Однако иногда это трудно реализовать, поскольку они зависят от общих ресурсов. Затем, при параллельном запуске тестов, нам нужно синхронизироваться по общим ресурсам в наших тестах. JUnit5 предоставляет нам такие механизмы в виде аннотации @ResourceLock .

Аналогично, как и раньше, создадим класс ParallelResourceLockUnitTest :

public class ParallelResourceLockUnitTest{
private List<String> resources;
@BeforeEach
void before() {
resources = new ArrayList<>();
resources.add("test");
}
@AfterEach
void after() {
resources.clear();
}
@Test
@ResourceLock(value = "resources")
public void first() throws Exception {
System.out.println("ParallelResourceLockUnitTest first() start => " + Thread.currentThread().getName());
resources.add("first");
System.out.println(resources);
Thread.sleep(500);
System.out.println("ParallelResourceLockUnitTest first() end => " + Thread.currentThread().getName());
}
@Test
@ResourceLock(value = "resources")
public void second() throws Exception {
System.out.println("ParallelResourceLockUnitTest second() start => " + Thread.currentThread().getName());
resources.add("second");
System.out.println(resources);
Thread.sleep(500);
System.out.println("ParallelResourceLockUnitTest second() end => " + Thread.currentThread().getName());
}
}

@ResourceLock позволяет нам указать, какой ресурс является общим, и тип блокировки, которую мы хотим использовать (по умолчанию — ResourceAccessMode.READ_WRITE ) . При текущей настройке механизм JUnit обнаружит, что наши тесты используют общий ресурс, и будет выполнять их последовательно:

ParallelResourceLockUnitTest second() start => ForkJoinPool-1-worker-5
[test, second]
ParallelResourceLockUnitTest second() end => ForkJoinPool-1-worker-5
ParallelResourceLockUnitTest first() start => ForkJoinPool-1-worker-19
[test, first]
ParallelResourceLockUnitTest first() end => ForkJoinPool-1-worker-19

6. Заключение

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

Как всегда, код из этой статьи можно найти на GitHub .