在 GitHub 上叉我

分叉选项和并行测试执行

选择正确的分叉策略和并行执行设置会对构建系统的内存需求和执行时间产生重大影响。

Surefire 提供了多种并行执行测试的选项,让您可以充分利用您可以使用的硬件。但特别是分叉也有助于保持较低的内存需求。

该页面将为您提供一些关于如何以最适合您的环境的方式配置测试执行的想法。

并行测试执行

基本上,maven-surefire-plugin 中有两种方法可以实现并行测试执行。

最明显的一种是使用parallel参数。可能的值取决于使用的测试提供程序。对于 JUnit 4.7 及更高版本,这可能是methods, classes, both, suites, suitesAndClasses, suitesAndMethods,classesAndMethodsall. 作为 JUnit 测试的先决条件,JUnit 运行器应该扩展org.junit.runners.ParentRunner. 如果没有通过 annotation 指定 runner @org.junit.runner.RunWith,则满足先决条件。

从 maven-surefire-plugin:2.16 开始,值 " both" 已被弃用,但它仍然可以使用并且行为与classesAndMethods.

有关详细信息,请参阅JUnitTestNG的示例页面。

使用以下参数配置并行度的扩展。该参数useUnlimitedThreads允许无限数量的线程。除非useUnlimitedThreads=true,该参数threadCount可以与可选参数一起使用perCoreThreadCount=true(默认为true)。参数useUnlimitedThreadsthreadCount将在为参数指定的值的上下文中解释parallel

从 maven-surefire-plugin:2.16 开始,可以使用一个或多个参数和对套件threadCountSuites、类或方法施加线程数限制。如果仅指定,maven-surefire-plugin 会尝试优化套件、类和方法的线程数,并重用线程以支持叶子,例如并行方法(可选地增加并发方法)。threadCountClassesthreadCountMethodsthreadCount

作为一个无限数量线程的例子,最多有三个并发线程来执行套件:parallel=all, useUnlimitedThreads=true, threadCountSuites=3.

在第二个例子中,并发方法的数量没有严格限制:parallel=classesAndMethods, threadCount=8, threadCountClasses=3. 这里并行方法的数量从 5 到 7 不等。因此parallel=all,但 和 的总和threadCountSuites不能threadCountClasses超过一定的 ( threadCount-1)。未指定线程数的叶子可以进行其他组合。确保叶子是最后一个来自 order suites-classes-methods in parallel

在第三个示例中,线程数表示一个比率,例如parallel=allthreadCount=16threadCountSuites=2threadCountClasses=3threadCountMethods=5。因此,并发套件将占 20%,并发类占 30%,并发方法占 50%。

最后,如果为 中的值指定了等效线程数threadCountuseUnlimitedThreads则不一定要配置 和parallel

maven-surefire-plugin 正在尝试重用线程,从而优化线程数,并更喜欢线程公平性。parallelOptimized默认情况下启用线程数量的优化,例如Suite runner 的数量不一定要浪费Suite的线程资源。如果threadCount使用,则具有无限线程数的叶子可能会加速,尤其是在测试阶段结束时。

参数parallelTestsTimeoutInSecondsparallelTestsTimeoutForcedInSeconds用于指定并行执行中的可选超时。如果超时,插件会打印带有错误行的摘要日志:“这些测试在关闭操作之前执行”,如果正在运行的线程被中断,则“这些测试不完整”

注意:根据 JUnit 运行器的设计,使用@Parameters@BeforeClass@AfterClass等注释的静态方法在父线程中调用。为了线程之间的内存可见性,同步方法。请参阅Java 内存模型 - JSR-133中的关键字:volatilesynchronizedimmutablefinal

使用该选项要记住的重要一点parallel是:并发发生在同一个 JVM 进程中。这在内存和执行时间方面是有效的,但您可能更容易受到竞争条件或其他意外且难以重现的行为的影响。

并行测试执行的另一种可能性是将参数设置为forkCount大于 1 的值。下一节将介绍有关此参数和相关reuseForks参数的详细信息。如果在没有分叉的情况下使用reuseForks=true(默认情况下)并在可重用的 JVM 中分叉测试类,可能会导致与@BeforeClass类初始化程序之间的共享静态代码相同的问题。因此设置可能会有所帮助,但它不能保证某些功能的正常功能,例如.parallelreuseForks=falseskipAfterFailureCount

并行测试执行和单线程执行

如上所述,parallel测试执行与特定线程数一起使用。从 maven-surefire-plugin:2.18 开始,您可以在 JUnit 测试的 Java 类(纯测试类、SuiteParameterized等)上应用JCIP注解,以便在单个 Thread 实例中执行它。线程的名称为maven-surefire-plugin@NotThreadSafe并在测试运行结束时执行。@net.jcip.annotations.NotThreadSafe

只需将项目依赖项 net.jcip:jcip-annotations:1.0或其他工件com.github.stephenc.jcip:jcip-annotations:1.0-1与 Apache License 2.0 一起使用。

<dependencies>
  [...]
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <!-- 4.7 or higher -->
    <version>4.7</version>
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>com.github.stephenc.jcip</groupId>
    <artifactId>jcip-annotations</artifactId>
    <version>1.0-1</version>
    <scope>test</scope>
  </dependency>
  [...]
</dependencies>

这样,带有注释的测试类的并行执行@NotThreadSafe在单线程实例中分叉(并不意味着分叉的 JVM 进程)。

如果SuiteParameterized用 注释@NotThreadSafe,则套件类在单线程中执行。您还可以注释 Suite 引用的单个测试类,并且Suite中其他未注释的测试类可以并行运行。通过这种方式,您可以隔离冲突的测试组,并且仍然并行运行它们的各个测试。

注意:根据 JUnit 运行器的设计,使用@Parameters@BeforeClass@AfterClass等注释的静态方法在父线程中调用。分配类以@NotThreadSafe Suite防止这种麻烦。如果您不想更改测试类的层次结构,您可以同步这些方法以提高内存可见性作为一种简单化的处理。请参阅Java 内存模型 - JSR-133中的关键字:volatilesynchronizedimmutablefinal

多模块 Maven 并行构建中的并行 maven-surefire-plugin 执行

Maven 核心允许与命令行选项并行构建多模块项目的模块-T。这增加了直接在 maven-surefire-plugin 中配置的并发程度。

分叉测试执行

该参数forkCount定义了 maven-surefire-plugin 将同时生成以执行测试的最大 JVM 进程数。它支持与 maven-core 中相同的语法-T:如果您以“C”终止该值,则该值将乘以系统中可用 CPU 内核的数量。例如forkCount=2.5C,在四核系统上将导致分叉多达十个执行测试的并发 JVM 进程。

该参数reuseForks用于定义是否在一个测试类之后终止生成的进程并为行(reuseForks=false)中的下一个测试创建一个新进程,或者是否重用这些进程来执行下一个测试(reuseForks=true)。

默认设置forkCount=1/ ,这reuseForks=true意味着 maven-surefire-plugin 创建一个新的 JVM 进程来执行一个 Maven 模块中的所有测试。

forkCount=1/reuseForks=false在自己的 JVM 进程中,一个接一个地执行每个测试类。它为测试执行创建了最高级别的分离,但它可能也会为您提供所有可用选项中最长的执行时间。将其视为最后的手段。

使用该argLine属性,您可以指定要传递给分叉 JVM 进程的附加参数,例如内存设置。来自主 maven 进程的系统属性变量也被传递给分叉的进程。此外,您可以使用该元素systemPropertyVariables指定要在测试执行期间添加到系统属性的变量和值。

${surefire.forkNumber}您可以在系统属性中argLine或系统属性中使用占位符(通过mvn test -D...和通过指定的属性systemPropertyVariables)。在执行测试之前,surefire插件将那个占位符替换为实际执行进程的数量,从1计数到forkCountMaven并行构建中最大并行执行次数的有效值,即-T命令行参数的有效值Maven 核心。

在禁用分叉 ( forkCount=0) 的情况下,占位符将替换为1

以下是一个示例配置,它使用最多三个执行测试然后终止的分叉进程。系统属性databaseSchema被传递给进程,该进程应指定在测试期间使用的数据库模式。这三个进程的值将是MY_TEST_SCHEMA_1MY_TEST_SCHEMA_2MY_TEST_SCHEMA_3。另外,通过指定自定义workingDirectory,每个进程都将在单独的工作目录中执行,以确保文件系统级别的隔离。

<plugins>
[...]
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M5</version>
    <configuration>
        <forkCount>3</forkCount>
        <reuseForks>true</reuseForks>
        <argLine>-Xmx1024m -XX:MaxPermSize=256m</argLine>
        <systemPropertyVariables>
            <databaseSchema>MY_TEST_SCHEMA_${surefire.forkNumber}</databaseSchema>
        </systemPropertyVariables>
        <workingDirectory>FORK_DIRECTORY_${surefire.forkNumber}</workingDirectory>
    </configuration>
  </plugin>
[...]
</plugins>

如果是在不同模块中进行测试的多模块项目,您还可以使用,例如,mvn -T 2 ...开始构建,产生${surefire.forkNumber}范围从 1 到 6 的值。

想象一下,您执行一些使用 JPA 上下文的测试,该上下文具有显着的初始启动时间。通过设置reuseForks=true,您可以重复使用该上下文进行连续测试。由于许多测试倾向于使用和访问相同的测试数据,因此您可以通过使用不同但统一的数据库模式来避免并发执行期间的数据库锁定。

端口号和文件名是在并发测试执行之间可能难以或不希望共享的资源的其他示例。

跨叉隔离报告目录

您可以在并行 fork JVM 进程中运行多个 TestNG 套件。在这种情况下,可以看到每个进程都创建了同名的 XML 报告文件,相互覆盖。为了防止这种情况发生,您可能需要更改每个 Suite XML 的报告目录。从版本 2.22.1 开始,您可以reportsDirectory使用 placeholder进行参数化${surefire.forkNumber}

[...]
<dependencies>
    [...]
    <dependency>
        <groupId>org.testng</groupId>
        <artifactId>testng</artifactId>
        <version>5.7</version>
        <classifier>jdk15</classifier>
    </dependency>
    [...]
</dependencies>
[...]
<build>
    <plugins>
            [...]
            <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>3.0.0-M5</version>
                    <configuration>
                            <forkCount>2</forkCount>
                            <reuseForks>false</reuseForks>
                            <reportsDirectory>target/surefire-reports-${surefire.forkNumber}</reportsDirectory>
                            <suiteXmlFiles>
                                    <suiteXmlFile>src/test/resources/Suite1.xml</suiteXmlFile>
                                    <suiteXmlFile>src/test/resources/Suite2.xml</suiteXmlFile>
                            </suiteXmlFiles>
                    </configuration>
            </plugin>
        [...]
    </plugins>
</build>

结合 forkCount 和并行

模式forkCount=0forkCount=1/reuseForks=true可以与 的可用设置自由组合parallel

由于reuseForks=false为每个测试类创建一个新的 JVM 进程,使用parallel=classes不会产生任何影响。不过,您仍然可以使用parallel=methods.

当使用reuseForks=trueforkCount大于一的值时,测试类被一个接一个地交给分叉的进程。因此,parallel=classes不会改变任何事情。但是,您可以使用parallel=methods:类在forkCount并发进程中执行,然后每个进程可以使用threadCount线程并行执行一个类的方法。

关于通过 与多模块并行 maven 构建的兼容性-T,唯一的限制是不能与forkCount=0.

当在没有 fork 的情况下运行并行 maven 构建时,所有系统属性在构建器线程和安全执行之间共享,因此线程在设置属性时会遇到竞争条件,例如baseDir,这可能会导致更改系统属性和意外的运行时行为。

将弃用的 forkMode 参数迁移到 forkCount 和 reuseForks

2.14 之前的 surefire 版本使用该参数forkMode来配置分叉。尽管仍支持该参数以实现向后兼容性,但强烈建议用户迁移其配置forkCountreuseForks改为使用。

鉴于以下映射,迁移非常简单:

旧环境 新环境
forkMode=once(默认) forkCount=1(默认),reuseForks=true(默认)
forkMode=always forkCount=1(默认),reuseForks=false
forkMode=never forkCount=0
forkMode=perthread,threadCount=N forkCount=N, ( reuseForks=false, 如果你没有那一套)

已知问题和限制

  • ${surefire.forkNumber}Maven 2.x 不支持传播(该变量将一直解析为空值)
  • ${surefire.forkNumber}workingDirectory自 maven-surefire-plugin:2.19 以来已在其中正确传播, SUREFIRE-1136中的更多详细信息