从JUnit 4迁移到JUnit 5:重要的区别和好处

从JUnit 4迁移到JUnit 5:重要的区别和好处

作者 | Brian McGlauflin
译者 | IT外文选刊

改进和新功能使JUnit 5引人注目。

JUnit 5 是对JUnit框架的一个强大而灵活的更新,它提供了各种改进和新的功能来组织和描述测试用例,并帮助理解测试结果。升级到JUnit 5非常简单快捷:只需更新你的项目依赖关系,就可以开始使用新功能。

如果你已经使用JUnit 4有一段时间了,迁移测试可能看起来是一项艰巨的任务。好消息是,你可能不需要转换任何测试;JUnit 5可以使用Vintage库运行JUnit 4测试。

话虽如此,但以下是开始使用JUnit 5编写新测试用例的四个强有力的理由:

  • JUnit 5利用了Java 8或更高版本的特性,例如lambda函数,使测试更强大,更容易维护。
  • JUnit 5为描述、组织和执行测试添加了一些非常有用的新功能。例如,测试得到了更好的显示名称,并且可以分层组织。
  • JUnit 5被组织成多个库,所以只将你需要的功能导入到你的项目中。通过Maven和Gradle等构建系统,包含合适的库很容易。
  • JUnit 5可以同时使用多个扩展,这是JUnit 4无法做到的(一次只能使用一个runner)。这意味着你可以轻松地将Spring扩展与其他扩展(如你自己的自定义扩展)结合起来。

从JUnit 4切换到JUnit 5是非常简单的,即使你有现有的JUnit 4测试,也是如此。大多数组织不需要将旧的JUnit测试转换为JUnit 5的测试,除非需要新的功能。在这种情况下,使用这些步骤:

  1. 将你的库和构建系统从JUnit 4更新到JUnit 5 。确保在你的测试运行时路径中包含 junit-vintage-engine 工件,以允许你现有的测试执行。
  2. 使用新的JUnit 5构造开始构建新的测试。
  3. (可选)将JUnit测试用例转换为JUnit 5的测试用例。

重要的区别

JUnit 5的测试看起来和JUnit 4的测试基本相同,但有几个不同之处你应该注意。

导入。 JUnit 5 使用新的 org.junit.jupiter 包。例如,org.junit.junit.Test变成了org.junit.jupiter.api.Test。

注解。 @Test 注解不再有参数,每个参数都被移到了一个函数中。例如,下面是如何在JUnit 4中表示预计一个测试会抛出异常的方法:

@Test(expected = Exception.class)
public void testThrowsException() throws Exception {
    // ...
}

在Junit5中,这个写法被改成了如下:

@Test
void testThrowsException() throws Exception {
    Assertions.assertThrows(Exception.class, () -> {
        //...
}

同样,超时也发生了变化。下面是JUnit 4中的一个例子:

@Test(timeout = 10)
public void testFailWithTimeout() throws InterruptedException {
    Thread.sleep(100);
}

在JUnit5中,它被改成了如下:

@Test
void testFailWithTimeout() throws InterruptedException {
    Assertions.assertTimeout(Duration.ofMillis(10), () -> Thread.sleep(100));
}

以下是其他的注解的变化:

  • @Before变成了@BeforeEach。
  • @After变成了@AfterEach。
  • @BeforeClass变成了@BeforeAll。
  • @AfterClass变成了@AfterAll。
  • @Ignore变成了@Disabled。
  • @Category变成了@Tag。
  • @Rule和@ClassRule没有了,用@ExtendWith和@RegisterExtension代替。

断言。 JUnit 5断言现在在org.junit.jupiter.api.Assertions中。大多数常见的断言,如assertEquals()和assertNotNull(),看起来和以前一样,但有一些不同。

  • 错误信息现在是最后一个参数,例如:assertEquals(“my message”,1,2)现在是assertEquals(1,2,”my message”)。
  • 大多数断言现在接受一个构造错误信息的lambda,只有当断言失败时才会被调用。
  • assertTimeout()和 assertTimeoutPreemptively()取代了 @Timeout 注释(在 JUnit 5 中有一个 @Timeout 注释,但它的工作方式与 JUnit 4 中不同)。
  • 有几个新的断言,如下文所述。

请注意,如果你愿意,你可以在JUnit 5测试中继续使用JUnit 4中的断言。

假设。 假设已被移至 org.junit.jupiter.api.Assumptions。

同样的假设也存在,但它们现在支持BooleanSupplier以及Hamcrest匹配器( hamcrest.org/ )来匹配条件。Lambdas(类型为 Executable)可以用来在条件满足时执行代码。

例如,这里是JUnit 4中的一个例子:

@Test
public void testNothingInParticular() throws Exception {
    Assume.assumeThat("foo", is("bar"));
    assertEquals(...);
}

在Junit5中,它变成了这样:

@Test
void testNothingInParticular() throws Exception {
    Assumptions.assumingThat("foo".equals(" bar"), () -> {
        assertEquals(...);
}

扩展JUnit

在JUnit 4中,自定义框架通常意味着使用@RunWith注释来指定一个自定义的运行器。使用多个运行器是有问题的,通常需要链式或使用@Rule。在JUnit 5中,这一点已经得到了简化和改进。

例如,在JUnit 4中,使用Spring框架构建测试看起来是这样的:

@RunWith(SpringJUnit4ClassRunner.class)
public class MyControllerTest {
    // ...
}

在JUnit 5中,你可以用Spring扩展来代替:

@ExtendWith(SpringExtension.class)
class MyControllerTest {
    // ...
}

@ExtendWith 注解是可重复的,这意味着多个扩展可以很容易地组合在一起。

你也可以通过创建一个类来实现org.junit.jupiter.api.extendWith中的一个或多个接口,然后用@ExtendWith将其添加到你的测试中,从而轻松定义你自己的自定义扩展。

将测试转换到JUnit 5

要将现有的JUnit 4测试转换为JUnit 5,请使用以下步骤,这应该对大多数测试都有效。

  1. 更新导入,删除JUnit 4并添加JUnit 5。例如,更新@Test注解的包名,更新断言的包名和类名(从Asserts到Assertions)。如果有编译错误也不要担心,因为完成下面的步骤应该可以解决。
  2. 在全局中用新的注解和类名替换旧的注解和类名。例如,将所有的@Before替换为@BeforeEach,将所有的Asserts替换为Assertions。
  3. 更新断言;任何提供消息的断言都需要将消息参数移到最后(当三个参数都是字符串时要特别注意!)。另外,更新超时和预期异常(见上面的例子)。
  4. 更新假设,如果你正在使用它们。
  5. 用适当的 @ExtendWith 注释替换 @RunWith、@Rule 或 @ClassRule 的任何实例。你可能需要在网上找到你所使用的扩展实例的更新文档。

注意,迁移参数化测试需要更多的重构,特别是如果你一直在使用JUnit 4参数化测试(JUnit 5参数化测试的格式更接近于JUnitParams),那么迁移参数化测试需要更多的重构。

新功能

到目前为止,我只讨论了现有的功能以及它的变化。但JUnit 5提供了大量的新功能,让你的测试更具有描述性和可维护性。

显示名称。 使用JUnit 5,你可以在类和方法中添加@DisplayName注释。这个名称在生成报告时使用,这使得描述测试的目的和追踪失败更容易,比如说:

@DisplayName("Test MyClass")
class MyClassTest {
    @Test
    @DisplayName("Verify MyClass.myMethod returns true")
    void testMyMethod() throws Exception {    
        // ...
}

你也可以使用显示名称生成器来处理你的测试类或方法,以你喜欢的任何格式生成测试名称。请参阅 JUnit文档 中的具体内容和示例。

断言。 JUnit 5引入了一些新的断言,比如以下这些:

  • assertIterableEquals()使用equals()对两个迭代项进行深度验证。
  • assertLinesMatch()验证两个字符串列表是否匹配;它接受期望参数中的正则表达式。
  • assertAll() 将多个断言分组在一起。附加的好处是所有的断言都会被执行,即使单个断言失败。
  • assertThrows()和 assertDoesNotThrow()取代了 @Test 注释中的预期属性。

嵌套测试。 JUnit 4中的测试套件是很有用的,但JUnit 5中的嵌套测试更容易设置和维护,它们能更好地描述测试组之间的关系,比如说:

@DisplayName("Verify MyClass")
class MyClassTest {
    MyClass underTest;
    @Test
    @DisplayName("can be instantiated")
    public void testConstructor() throws Exception {    
        new MyClass();
    @Nested
    @DisplayName("with initialization")
    class WithInitialization {
        @BeforeEach
        void setup() {
            underTest = new MyClass();
            underTest.init("foo");
        @Test
        @DisplayName("myMethod returns true")
        void testMyMethod() {
            assertTrue(underTest.myMethod());