1. Введение
Хотя последовательное выполнение тестов в большинстве случаев работает нормально, мы можем захотеть распараллелить их, чтобы ускорить процесс.
В этом руководстве мы расскажем, как распараллелить тесты с помощью JUnit и подключаемого модуля Maven Surefire. Сначала запустим все тесты в одном JVM-процессе, потом попробуем с многомодульным проектом.
2. Зависимости Maven
Начнем с импорта необходимых зависимостей. Нам нужно будет использовать JUnit 4.7 или новее вместе с Surefire 2.16 или новее:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
В двух словах, Surefire предоставляет два способа параллельного выполнения тестов:
- Многопоточность внутри одного процесса JVM
- Разветвление нескольких процессов JVM
3. Запуск параллельных тестов
Чтобы запустить тест параллельно, мы должны использовать средство запуска тестов, которое расширяет org.junit.runners.ParentRunner
.
Тем не менее, даже тесты, которые не объявляют обработчика тестов явным образом, работают, поскольку обработчик по умолчанию расширяет этот класс.
Далее, чтобы продемонстрировать параллельное выполнение тестов, мы будем использовать набор тестов с двумя тестовыми классами, каждый из которых имеет несколько методов. На самом деле подойдет любая стандартная реализация набора тестов JUnit.
3.1. Использование параллельного параметра
Во-первых, давайте включим параллельное поведение в Surefire, используя параметр parallel
. В нем указывается уровень детализации, на котором мы хотели бы применить параллелизм.
Возможные значения:
методы —
запускает тестовые методы в отдельных потокахклассы —
запускает тестовые классы в отдельных потокахclassAndMethods —
запускает классы и методы в отдельных потокахSuites —
запускает Suite параллельноsuitesAndClasses —
запускает наборы и классы в отдельных потоках .suitesAndMethods —
создает отдельные потоки для классов и для методовall —
запускает наборы, классы и методы в отдельных потоках.
В нашем примере мы используем все
:
<configuration>
<parallel>all</parallel>
</configuration>
Во-вторых, давайте определим общее количество потоков, которые мы хотим создать Surefire. Мы можем сделать это двумя способами:
Использование threadCount
, которое определяет максимальное количество потоков, создаваемых Surefire:
<threadCount>10</threadCount>
Или с помощью параметра useUnlimitedThreads
, где создается один поток на ядро ЦП:
<useUnlimitedThreads>true</useUnlimitedThreads>
По умолчанию threadCount
относится к ядру ЦП. Мы можем использовать параметр perCoreThreadCount
, чтобы включить или отключить это поведение:
<perCoreThreadCount>true</perCoreThreadCount>
3.2. Использование ограничений количества потоков
Теперь предположим, что мы хотим определить количество потоков, которые необходимо создать на уровне метода, класса и пакета. Мы можем сделать это с помощью параметров threadCountMethods
, threadCountClasses
и threadCountSuites
.
Объединим эти параметры с threadCount
из предыдущей конфигурации: ``
<threadCountSuites>2</threadCountSuites>
<threadCountClasses>2</threadCountClasses>
<threadCountMethods>6</threadCountMethods>
Поскольку мы использовали все
параллельно ,
мы определили количество потоков для методов, наборов и классов. Однако не обязательно определять параметр leaf. Surefire вычисляет количество потоков, которые следует использовать, если параметры листа опущены.
Например, если threadCountMethods
опущен, нам просто нужно убедиться, что threadCount
> threadCountClasses
+ threadCountSuites.
Иногда нам может понадобиться ограничить количество потоков, созданных для классов, комплектов или методов, даже если мы используем неограниченное количество потоков.
Мы можем применять ограничения на количество потоков и в таких случаях:
<useUnlimitedThreads>true</useUnlimitedThreads>
<threadCountClasses>2</threadCountClasses>
3.3. Установка тайм-аутов
Иногда нам может понадобиться убедиться, что выполнение теста ограничено по времени.
Для этого мы можем использовать параметр parallelTestTimeoutForcedInSeconds
. Это прервет текущие запущенные потоки и не будет выполнять ни один из потоков в очереди по истечении времени ожидания:
<parallelTestTimeoutForcedInSeconds>5</parallelTestTimeoutForcedInSeconds>
Другой вариант — использовать parallelTestTimeoutInSeconds
.
В этом случае будет остановлено выполнение только потоков в очереди:
<parallelTestTimeoutInSeconds>3.5</parallelTestTimeoutInSeconds>
Тем не менее, в обоих случаях тесты завершатся с сообщением об ошибке по истечении времени ожидания.
3.4. Предостережения
Surefire вызывает статические методы, аннотированные @Parameters
, @BeforeClass
и @AfterClass
в родительском потоке. Таким образом, перед параллельным запуском тестов обязательно проверяйте потенциальные несоответствия памяти или условия гонки.
Кроме того, тесты, изменяющие общее состояние, определенно не являются хорошими кандидатами для параллельного выполнения.
4. Выполнение тестов в многомодульных проектах Maven
До сих пор мы сосредоточились на параллельном выполнении тестов в модуле Maven.
Но допустим, у нас есть несколько модулей в проекте Maven. Поскольку эти модули строятся последовательно, тесты для каждого модуля также выполняются последовательно.
Мы можем изменить это поведение по умолчанию, используя параметр Maven -T
, который создает модули параллельно . Это можно сделать двумя способами.
Мы можем либо указать точное количество потоков, которые будут использоваться при сборке проекта:
mvn -T 4 surefire:test
Или используйте переносную версию и укажите количество потоков, которые нужно создать на ядро ЦП:
mvn -T 1C surefire:test
В любом случае мы можем ускорить тесты, а также сократить время выполнения.
5. Разветвление JVM
При параллельном выполнении теста с помощью параметра parallel
параллелизм происходит внутри процесса JVM с использованием потоков .
Поскольку потоки совместно используют одно и то же пространство памяти, это может быть эффективным с точки зрения памяти и скорости. Однако мы можем столкнуться с непредвиденными условиями гонки или другими малозаметными ошибками тестов, связанными с параллелизмом. Как оказалось, совместное использование одного и того же пространства памяти может быть как благословением, так и проклятием.
Чтобы предотвратить проблемы параллелизма на уровне потоков, Surefire предоставляет еще один режим параллельного выполнения тестов: разветвление и параллелизм на уровне процесса . Идея разветвленных процессов на самом деле довольно проста. Вместо того, чтобы создавать несколько потоков и распределять методы тестирования между ними, surefire создает новые процессы и выполняет такое же распределение.
Поскольку между различными процессами нет общей памяти, мы не будем страдать от этих незаметных ошибок параллелизма. Конечно, это происходит за счет большего использования памяти и немного меньшей скорости.
В любом случае, чтобы включить разветвление, нам просто нужно использовать свойство forkCount
и установить для него любое положительное значение:
<forkCount>3</forkCount>
Здесь surefire создаст не более трех ответвлений от JVM и запустит в них тесты. Значение по умолчанию для forkCount
равно единице, что означает, что maven-surefire-plugin
создает один новый процесс JVM для выполнения всех тестов в одном модуле Maven.
Свойство forkCount
поддерживает тот же синтаксис, что и -T
. То есть, если мы добавим C
к значению, это значение будет умножено на количество доступных ядер ЦП в нашей системе. Например:
<forkCount>2.5C</forkCount>
Затем на двухъядерной машине Surefire может создать не более пяти ответвлений для параллельного выполнения теста.
По умолчанию Surefire повторно использует созданные форки для других тестов . Однако, если мы установим для свойства reuseForks
значение false
, он уничтожит каждую вилку после запуска одного тестового класса.
Кроме того, чтобы отключить разветвление, мы можем установить forkCount
равным нулю.
6. Заключение
Подводя итог, мы начали с включения многопоточного поведения и определения степени параллелизма с помощью параметра parallel
. Впоследствии мы применили ограничения на количество потоков, которые должна создавать Surefire. Позже мы устанавливаем параметры тайм-аута, чтобы контролировать время выполнения теста.
Наконец, мы рассмотрели, как можно сократить время выполнения сборки и, следовательно, время выполнения тестов в многомодульных проектах Maven.
Как всегда, представленный здесь код доступен на GitHub .