对于所有的 Java 开发人员来说,你可以没有听说过 Spring 或是 Hibernate 框架,但是一定听说过 JUnit。JUnit 作为 Java 单元测试的鼻祖与事实上的标准,在非常多的项目中被使用。即便新兴的单元测试框架,如 TestNG 等,不断出现,JUnit 的重要性仍然是不言而喻的。目前广泛使用的是 JUnit 4 版本,而 JUnit 即将迎来它的最新版本 JUnit 5。JUnit 5 在增加了很多的新特性的同时,又保持了对 JUnit 4 的向后兼容性。本文对 JUnit 5 进行了详细的介绍。
JUnit 5 简介
与之前的版本不同,JUnit 5 由三个不同的模块组成。第一个模块是 JUnit 平台,其主要作用是在 JVM 上启动测试框架。它定义了一个抽象的 TestEngine API 来定义运行在平台上的测试框架,同时还支持通过命令行、Gradle 和 Maven 来运行平台。第二个模块是 JUnit Jupiter,包含了 JUnit 5 最新的编程模型和扩展机制。第三个模块是 JUnit Vintage,允许在平台上运行 JUnit 3 和 JUnit 4 的测试用例。
JUnit 5 对 Java 运行环境的最低要求是 Java 8。可以在 Eclipse 和 IntelliJ IDEA 上运行 JUnit 5 测试。本文的示例基于 IntelliJ IDEA 上开发,并使用 Gradle 作为构建工具。不过目前 IDE 对 JUnit 5 的支持还比较有限,只有最新版本的 IntelliJ IDEA 原生支持,在其它 IDE 上需要使用命令行工具来运行。
编写测试用例
JUnit 5 对编写单元测试用例的方式做了一系列的改进,如下介绍。
JUnit 5 注解
JUnit 5 提供了一些常用的注解在编写测试用例的时候使用。其中的一些注解和 JUnit 4 的注解有相同的名称,不过所在的 Java 包变成了 org.junit.jupiter.api。常用的注解见表 1。
表 1. JUnit 5 常用注解
清单 1 中给出了使用这些注解编写的单元测试用例。
清单 1. 使用常用注解的单元测试用例
@DisplayName("Calculator") public class CalculatorTest { private Calculator calculator; @BeforeAll public static void init() { System.out.println("Start testing"); } @BeforeEach public void create() { this.calculator = new Calculator(); } @AfterEach public void destroy() { this.calculator = null; } @AfterAll public static void cleanup() { System.out.println("Finish testing"); } @Test @DisplayName("Test 1 + 2 = 3") public void testAdd() { assertEquals(3, this.calculator.add(1, 2)); } @Test @DisplayName("Test 3 - 2 = 1") public void testSubtract() { assertEquals(1, this.calculator.subtract(3, 2)); } @Disabled @Test @DisplayName("disabled test") public void ignoredTest() { System.out.println("This test is disabled"); } } |
在这些注解中,最实用的应该是@DisplayName。通过@DisplayName,开发人员可以为每个测试用例添加更具体的名字,更容易传达用例所要测试的内容。
通过@Tag 注解可以为测试类或方法添加标签,但是不同的标签只是通过字符串来进行区分,并不是类型安全的。一个拼写错误就可能造成标签没有被正确应用。更好的做法是使用类型安全的元注解(meta annotation)。编译器会对元注解标签的正确性进行验证,从而减少无意的错误。清单 2 中定义了元注解标签@Remote,对应于标签 remote。
清单 2. 元注解标签
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Tag("remote") public @interface Remote { } |
清单 3 中展示了@Remote 的用法。使用@Remote 的作用等同于@Tag("Remote"),为 testGetUser 方法添加了标签 remote。使用@Remote 不仅提高了代码的可读性,也可以避免无意的拼写错误带来的问题。
清单 3. 使用元注解标签
@DisplayName("Remote test") public class RemoteTest { @Test @Remote public void testGetUser() { System.out.println("Get user"); } } |
JUnit 5 断言
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:
第一类是简单断言,用来对单个值进行简单的验证,常用的方法见表 2。
表 2. 常用的断言方法
这些方法都有多个重载方法,可以提供额外的消息来作为断言不满足时的提示消息,还可以接受 Java 8 中的 Supplier 接口来获取要判断的值和显示的消息。清单 4 中给出了简单断言的使用示例。
清单 4. 简单断言
@Test @DisplayName("simple assertion") public void simple() { assertEquals(3, 1 + 2, "simple math"); assertNotEquals(3, 1 + 1); assertNotSame(new Object(), new Object()); Object obj = new Object(); assertSame(obj, obj); assertFalse(1 > 2); assertTrue(1 < 2); assertNull(null); assertNotNull(new Object()); } |
第二类是通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等,如清单 5 所示。
清单 5. assertArrayEquals 方法的示例
@Test @DisplayName("array assertion") public void array() { assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); } |
第三类是通过 assertAll 方法来判断一组断言是否满足。assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言,如清单 6 所示。
清单 6. assertAll 方法的示例
@Test @DisplayName("assert all") public void all() { assertAll("Math", () -> assertEquals(2, 1 + 1), () -> assertTrue(1 > 0) ); } |
第四类是通过 assertThrows 或 expectThrows 来判断是否抛出期望的异常类型。两个方法的参数都是所期望的异常类型和对应的 Executable 接口的实现对象,区别在于 expectThrows 方法会返回抛出的异常对象。在清单 7 中,1/0 会抛出 ArithmeticException 异常,assertThrows 用来验证这一点。
清单 7. assertThrows 和 expectThrows 方法的示例
@Test @DisplayName("throws exception") public void exception() { assertThrows(ArithmeticException.class, () -> System.out.println(1 / 0)); } |
第五类是 fail 方法,用来使一个测试方法失败。清单 8 中的测试会直接失败。
清单 8. 通过 fail 方法直接使得测试失败
@Test @DisplayName("fail") public void shouldFail() { fail("This should fail"); } |
JUnit 5 前置条件
JUnit 5 中的前置条件(assumptions)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。在清单 9 中,assumeTrue 和 assumFalse 确保给定的条件为 true 或 false,不满足条件会使得测试执行终止。assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。
清单 9. JUnit 5 前置条件
@DisplayName("Assumptions") public class AssumptionsTest { private final String environment = "DEV"; @Test @DisplayName("simple") public void simpleAssume() { assumeTrue(Objects.equals(this.environment, "DEV")); assumeFalse(() -> Objects.equals(this.environment, "PROD")); } @Test @DisplayName("assume then do") public void assumeThenDo() { assumingThat( Objects.equals(this.environment, "DEV"), () -> System.out.println("In DEV") ); } } |