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
), мы получаем четыре различных режима выполнения:
- (
same_thread, same_thread
) — все тесты запускаются последовательно - (
same_thread, concurrent
) — тесты из одного класса выполняются последовательно, но несколько классов выполняются параллельно - (
concurrent, same_thread
) — тесты одного класса выполняются параллельно, но каждый класс выполняется отдельно - (
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 .