从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());
        }
    }
}

在上面的例子中,你可以看到,我对所有与MyClass相关的测试都使用一个类。我可以验证该类在外部测试类中是可以实例化的,而我对所有MyClass被实例化和初始化的测试都使用嵌套的内部类。@BeforeEach方法只适用于嵌套类中的测试。

测试和类的@DisplayNames注解表示了测试的目的和组织方式。这有助于理解测试报告,因为你可以看到测试是在什么条件下执行的(初始化后验证MyClass),以及测试要验证什么(myMethod返回true)。这是JUnit 5的一个很好的测试设计模式。

测试的参数化。测试参数化在JUnit 4中就已经存在,有内置的库如JUnit4Parameterized或第三方库如JUnitParams等。在JUnit 5中,参数化测试完全内置,并采用了JUnit4Parameterized和JUnitParams等一些最好的特性。例子:

@ParameterizedTest
@ValueSource(strings = {"foo", "bar"})
@NullAndEmptySource
void myParameterizedTest(String arg) {
    underTest.performAction(arg);
}

其格式看起来像JUnitParams,其中参数直接传递给测试方法。注意,要测试的值可以来自多个不同的来源。这里,我只用了一个参数,所以使用@ValueSource很方便。@EmptySource和@NullSource分别表示你要在要运行的值列表中添加一个空字符串和一个空值(如果你使用这两个值,你可以把它们组合在一起,如上所示)。还有其他多个值源,比如@EnumSource和@ArgumentsSource(一种自定义值提供者)。如果你需要一个以上的参数,也可以使用@MethodSource或@CsvSource。

在JUnit 5中添加的另一个测试类型是@RepeatedTest,在这里,一个测试被重复指定次数的测试。

测试执行条件。JUnit 5 提供了 ExecutionCondition 扩展 API 来有条件地启用或禁用一个测试或容器(测试类)。这就像在测试上使用@Disabled一样,但它可以定义自定义条件。有多个内置的条件,比如说这些条件:

  • @EnabledOnOs和@DisabledOnOs。仅在指定的操作系统上启用或禁用测试。
  • @EnabledOnJre和@DisabledOnJre。指定特定版本的Java测试应该启用或禁用。
  • @EnabledIfSystemProperty: 启用基于JVM系统属性值的测试。
  • @EnabledIf: 使用脚本逻辑,在满足脚本条件的情况下,使用脚本逻辑启用测试。

测试模板。测试模板不是常规测试;它们定义了一组要执行的步骤,然后可以在其他地方使用特定的调用上下文执行。这意味着你可以定义一次测试模板,然后在运行时建立一个调用上下文列表来运行该测试。有关详细信息和示例,请参阅文档

动态测试。动态测试就像测试模板,要运行的测试是在运行时生成的。然而,测试模板是用一组特定的步骤来定义并运行多次,而动态测试使用相同的调用上下文,但可以执行不同的逻辑。动态测试的一个用途是将一个抽象对象的列表流化,并根据它们的具体类型为每个对象执行一组单独的断言。文档中有一些很好的例子。

结论

尽管你可能不需要将旧的JUnit 4测试转换为JUnit 5,除非你想使用新的JUnit 5功能,但也有令人信服的理由让你切换到JUnit 5。例如,JUnit 5的测试功能更强大,更容易维护。此外,JUnit 5提供了许多有用的新特性,只有你使用的特性才会被导入,你可以使用多个扩展,甚至可以创建自己的自定义扩展。这些变化和新功能一起,为JUnit框架提供了一个强大而灵活的更新。


外文链接:

版权声明:

本译文仅用于学习、研究和交流目的,欢迎非商业转载。转载请注明出处、译者和IT外文选刊的完整链接。

更多优质IT外文在“IT外文选刊”(公众号)

发布于 2020-05-30 20:23