JUnit测试类的顺序

30
我有一个使用Maven和JUnit进行测试的Java应用程序,其中包含failsafe和surefire插件。
我有超过2000个集成测试。 为了加快测试运行速度,我使用failsafe jvmfork并行运行我的测试。 我有一些比较耗时的测试类,它们通常在测试执行的最后运行,这会减慢我的CI验证过程。 文件安全的运行顺序:balanced对我来说是一个不错的选择,但由于jvmfork的原因我无法使用它。 重命名测试类或将其移动到另一个包并按字母顺序运行不是一个选项。 有没有建议如何在验证过程的开始运行我的耗时测试类?

我理解你的意思,但我不明白为什么这是个问题。如果在最后运行大量测试,会有什么变化? - Arnaud Claudel
那么,有什么区别呢? - Arnaud Claudel
1
Jenkins流水线是否不可行?你可以像@jackkobec所说的那样为慢速测试创建一个新类别,并创建一个具有并行阶段的流水线(https://jenkins.io/blog/2017/09/25/declarative-1/)。例如,首先您将拥有两个并行阶段,一个运行慢速类别测试,另一个运行所有其他测试。 - gybandi
1
@gybandi 这对我来说不是最好的解决方案。 如果我有10个线程来运行我的测试,并且我定义了2个并行的流水线阶段来运行慢速和其他测试,我为这些阶段分配了5-5个线程,当其中一个阶段完成运行时,我无法重新分配空闲的5个线程以更快地完成正在运行的阶段。 - Sándor Juhos
升级到 JUnit5 对你来说是一个选择吗?根据此链接 ,他们在那里添加了一些自定义排序功能。 - second
显示剩余5条评论
7个回答

46
在JUnit 5(从版本5.8.0开始)中,测试类也可以进行排序。

src/test/resources/junit-platform.properties:

# ClassOrderer$OrderAnnotation sorts classes based on their @Order annotation
junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$OrderAnnotation

其他Junit内置的类排序实现方式有:
org.junit.jupiter.api.ClassOrderer$ClassName
org.junit.jupiter.api.ClassOrderer$DisplayName
org.junit.jupiter.api.ClassOrderer$Random

除了junit-platform.properties文件之外,还有其他设置配置参数的方法,请参阅JUnit 5用户指南

您还可以提供自己的排序器。它必须实现ClassOrderer接口:

package foo;
public class MyOrderer implements ClassOrderer {
    @Override
    public void orderClasses(ClassOrdererContext context) {
        Collections.shuffle(context.getClassDescriptors());
    }
}

junit.jupiter.testclass.order.default=foo.MyOrderer

请注意,@Nested 测试类不能按照 ClassOrderer 进行排序。
请参考 JUnit 5 的 文档 和 ClassOrderer 的 API 文档 以了解更多信息。

我尝试了,是的,它可以工作!最终,他们支持类排序。 - Aksoy
有人尝试启用并行测试执行吗?例如:test {maxParallelForks = 4} 对我来说,不幸的是,按类排序无法正常工作。 - Ben

6

从junit的5.8.0-M1版本开始,有一个解决方案可供使用。

基本上,您需要创建自己的排序器。我也做了这样的事情。

这是一个注释,您将在测试类中使用:

@Retention(RetentionPolicy.RUNTIME)
public @interface TestClassesOrder {
    public int value() default Integer.MAX_VALUE;
}

那么,您需要创建一个类来实现org.junit.jupiter.api.ClassOrderer。

public class AnnotationTestsOrderer implements ClassOrderer {
    @Override
    public void orderClasses(ClassOrdererContext context) {
        Collections.sort(context.getClassDescriptors(), new Comparator<ClassDescriptor>() {
            @Override
            public int compare(ClassDescriptor o1, ClassDescriptor o2) {
                TestClassesOrder a1 = o1.getTestClass().getDeclaredAnnotation(TestClassesOrder.class);
                TestClassesOrder a2 = o2.getTestClass().getDeclaredAnnotation(TestClassesOrder.class);
                if (a1 == null) {
                    return 1;
                }

                if (a2 == null) {
                    return -1;
                }
                if (a1.value() < a2.value()) {
                    return -1;
                }

                if (a1.value() == a2.value()) {
                    return 0;
                }

                if (a1.value() > a2.value()) {
                    return 1;
                }
                return 0;
            }
        });
    }
}

要让它运行起来,您需要告诉junit使用哪个类来进行描述符排序。因此,您需要创建名为“junit-platform.properties”的文件,并将其放在资源文件夹中。在该文件中,您只需要一行包含排序器类的代码即可:
junit.jupiter.testclass.order.default=org.example.tests.AnnotationTestOrderer

现在你可以像使用Order注解一样,在类级别上使用你的订购者注解:

@TestClassesOrder(1)
class Tests {...}

@TestClassesOrder(2)
class MainTests {...}

@TestClassesOrder(3)
class EndToEndTests {...}

我希望能够帮助到某些人。

你的 @TestClassesOrder 示例中语法有误。 - John John Pichler
@JohnJohnPichler 我不确定哪里出了问题。在我的电脑上没有任何错误。 - Morph21
2
使用“junit-platform.properties”文件而不是常规的“application.properties”文件的提及非常准确。 - Dima Goldin

6
我尝试了以下答案组合:
第二个答案基于这个类库的这些Github项目,该项目根据BSD-2许可证提供。
我定义了一些测试类:
public class LongRunningTest {

    @Test
    public void test() {

        System.out.println(Thread.currentThread().getName() + ":\tlong test - started");

        long time = System.currentTimeMillis();
        do {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        } while(System.currentTimeMillis() - time < 1000);

        System.out.println(Thread.currentThread().getName() + ":\tlong test - done");
    }
}

@Concurrent
public class FastRunningTest1 {

    @Test
    public void test1() {
        try {
            Thread.sleep(250);
        } catch (InterruptedException e) {
        }

        System.out.println(Thread.currentThread().getName() + ":\tfrt1-test1 - done");
    }

    // +7 more repetions of the same method
}

然后我定义了测试套件:
(FastRunningTest2 是第一个类的副本,输出进行了调整)

@SuiteClasses({LongRunningTest.class, LongRunningTest.class})
@RunWith(Suite.class)
public class SuiteOne {}

@SuiteClasses({FastRunningTest1.class, FastRunningTest2.class})
@RunWith(Suite.class)
public class SuiteTwo {}

@SuiteClasses({SuiteOne.class, SuiteTwo.class})
@RunWith(ConcurrentSuite.class)
public class TopLevelSuite {}

当我执行TopLevelSuite时,输出如下: TopLevelSuite-1-thread-1: long test - started FastRunningTest1-1-thread-4: frt1-test4 - done FastRunningTest1-1-thread-2: frt1-test2 - done FastRunningTest1-1-thread-1: frt1-test1 - done FastRunningTest1-1-thread-3: frt1-test3 - done FastRunningTest1-1-thread-5: frt1-test5 - done FastRunningTest1-1-thread-3: frt1-test6 - done FastRunningTest1-1-thread-1: frt1-test8 - done FastRunningTest1-1-thread-5: frt1-test7 - done FastRunningTest2-2-thread-1: frt2-test1 - done FastRunningTest2-2-thread-2: frt2-test2 - done FastRunningTest2-2-thread-5: frt2-test5 - done FastRunningTest2-2-thread-3: frt2-test3 - done FastRunningTest2-2-thread-4: frt2-test4 - done TopLevelSuite-1-thread-1: long test - done TopLevelSuite-1-thread-1: long test - started FastRunningTest2-2-thread-5: frt2-test8 - done FastRunningTest2-2-thread-2: frt2-test6 - done FastRunningTest2-2-thread-1: frt2-test7 - done TopLevelSuite-1-thread-1: long test - done 这基本上显示了LongRunningTestFastRunningTests并行执行。并行执行的默认线程数由Concurrent注释定义,默认值为5,可以在FastRunningTests的并行执行输出中看到。
缺点是这些Threads不共享FastRunningTest1FastRunningTest2之间。
这种行为表明你想做的事情“有些”可能,但这是否适用于你当前的设置是一个不同的问题。
此外,我不确定这是否值得努力,
- 因为您需要手动准备这些TestSuites(或编写自动生成它们的内容) - 您需要为所有这些类定义并发注释(也许每个类的线程数量都不同)。

由于这基本上表明可以定义类的执行顺序并触发它们的并行执行,所以应该也可能使整个过程只使用一个ThreadPool(但我不确定这会有什么影响)。

由于整个概念是基于ThreadPoolExecutor构建的,使用PriorityBlockingQueue将长时间运行的任务赋予更高的优先级,您将更接近执行长时间运行的测试的理想结果。


我做了更多实验,并实现了自己的自定义套件运行程序和JUnit运行程序。其背后的想法是让您的JUnitRunner将测试提交到由单个ThreadPoolExecutor处理的队列中。由于我没有在RunnerScheduler#finish方法中实现阻塞操作,因此在执行开始之前,所有类的测试都被传递到队列中。(如果涉及更多的测试类和方法,则情况可能会有所不同)。

至少证明了一点:如果您真的想要这样做,那么您可以在JUnit的这个级别上进行更改。

我的poc代码有点凌乱,太长了,放在这里不太合适,但如果有人感兴趣,我可以将它推送到GitHub项目中。


4
在我提出建议之前,让我总结一下所有内容。
1. 集成测试很慢。这很正常且自然。 2. CI流程不会运行需要系统部署的测试,因为CI流程中没有部署。我们关心部署在CD流程中。 因此,我认为您的集成测试不会涉及部署。 3. CI流程首先运行单元测试。单元测试非常快,因为它们只使用内存。 我们从单元测试中得到了良好和快速的反馈。
目前,我们确定快速获得反馈并没有问题。但我们仍然想更快地运行集成测试。 我建议采取以下解决方案:
1. 改进现有测试。它们经常无效且可以显著加快速度。 2. 在后台运行集成测试(即不等待实时反馈)。它们比单元测试慢得多,这很自然。 3. 将集成测试分组并单独运行,如果您需要更快地获得其中一些反馈。 4. 在不同JVM中运行集成测试。不是在同一个JVM中的不同线程! 在这种情况下,您不需要考虑线程安全性,也不应该考虑它。 5. 在不同的机器上运行集成测试等等。
我曾与许多不同的项目合作(其中一些CI流程运行了48个小时),前三个步骤足以解决问题(即使对于疯狂的情况)。在有很好的测试的情况下,很少需要第4步。第5步是针对非常特定的情况的。
您会发现我的建议涉及流程而不是工具,因为问题在于流程。 经常出现人们忽略根本原因并尝试调整工具(在这种情况下是Maven)。他们获得了装饰性的改进,但维护成本高昂。

4
在我们的项目中,我们创建了一些标记接口(例如)。
public interface SlowTestsCategory {}

并将其放入具有缓慢测试的测试类中JUnit的@Category注释中。
@Category(SlowTestsCategory.class)

接下来,我们创建了一些特殊任务供Gradle按类别或自定义顺序运行测试:

task unitTest(type: Test) {
  description = 'description.'
  group = 'groupName'

  useJUnit {
    includeCategories 'package.SlowTestsCategory'
    excludeCategories 'package.ExcludedCategory'
  }
}

这个解决方案使用Gradle来提供支持,但它可能对您有所帮助。


0
这里还有另一种可能的方式,在某些情况下可能会很有用。
例如,如果我希望所有的UI测试(执行时间较长)在我的常规单元测试之后运行。
为此,可以创建一个单独的源集,并在常规测试之后运行它。

build.gradle.kts:

sourceSets {
    create("uiTest") {
        // Adds files from the main source set to the compile classpath and runtime classpath of this new source set.
        // sourceSets.main.output is a collection of all the directories containing compiled main classes and resources
        compileClasspath += sourceSets.main.get().output
        runtimeClasspath += sourceSets.main.get().output
    }
}
// Makes the uiTest configurations extend from test configurations,
// which means that all the declared dependencies of the test code (and transitively the main as well)
// also become dependencies of this new configuration
val uiTestImplementation by configurations.getting {
    extendsFrom(configurations.testImplementation.get())
}
val uiTestRuntimeOnly by configurations.getting {
    extendsFrom(configurations.testRuntimeOnly.get())
}
val uiTest = task<Test>("uiTest") {
    description = "Runs UI tests."
    group = "verification"

    testClassesDirs = sourceSets["uiTest"].output.classesDirs
    classpath = sourceSets["uiTest"].runtimeClasspath
    mustRunAfter("test") // The important part

    testLogging {
        events(TestLogEvent.PASSED)
    }
}
tasks.check { dependsOn(uiTest) }

tasks.withType<Test> {
    useJUnitPlatform() // Enables JUnit 5 (along with JUnit 4) tests
}

src
  --- main
      --- java
      --- resources
  --- test
      --- java
      --- resources
  --- uiTest
      --- java
      --- resources

src/uiTest/java/...目录中创建您的UI测试,就像在src/test/java/...中一样。

现在,如果您执行check Gradle任务(./gradlew check)(这只是一个执行其他测试任务的任务),UI测试将在常规单元测试之后执行。

附注:

  • 通过执行./gradlew uiTest任务可以执行UI测试
  • 通过执行./gradlew test任务可以执行单元测试

-6

在 Junit 5 中,您可以使用注释来设置所需的测试顺序:

根据 Junit 5 的用户指南: https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-execution-order

import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {

    @Test
    @Order(1)
    void nullValues() {
        // perform assertions against null values
    }

    @Test
    @Order(2)
    void emptyValues() {
        // perform assertions against empty values
    }

    @Test
    @Order(3)
    void validValues() {
        // perform assertions against valid values
    }

}

升级到 Junit5 可以相对容易地完成,帖子开头的链接中包含了您可能需要的所有信息。


谢谢,但我需要类的排序,而不是方法的排序。 - Sándor Juhos
1
非常抱歉,我误解了你的问题。 - agent_Karma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接