上一篇 JUnit 5 快速上手(从 JUnit 4 到 JUnit 5) 介绍了如何在一个项目中同时使用 JUnit 4 和 JUnit 5。现在来开始了解 JUnit 5 的新特性. 我们现在的项目基本是用 Maven 来管理依赖,在 Maven 项目中如何引入 JUnit 5 可以参考官方例子 junit5-maven-consumer. 我们知道 JUnit 5 包括三个模块,不用 JUnit 4 的话只要 Platform 和 Jupiter, 而 Jupiter Maven 模块本身依赖于 JUnit Platform, 因此应用 JUnit 5 的项目 Maven 配置就是
1 2 3 4 5 6 |
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.0.0</version> <scope>test</scope> </dependency> |
这样在当前的 IntelliJ IDEA(2017.2.4) 可以执行 JUnit 5 的测试用例。但要让 Maven 找到 JUnit 5 的测试用例,还得在 pom.xml
中加上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.19.1</version> <dependencies> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-surefire-provider</artifactId> <version>1.0.0</version> </dependency> </dependencies> </plugin> </plugins> </build> |
这是由于 JUnit 5 的 API 从包 org.junit
变到了 org.junit.jupiter.api
,而现在 maven-surefire-plugin
只能识别 org.junit.Test
标注的测试用例。
一. JUnit 5 的注解
JUnit 5 注解 | 相应 JUnit 4 中的注解 | 说明 |
@Test | @Test |
在 JUnit 4, 5 混合的项目,特别注意包名. 如果是 JUnit 4 的 @Test, 那么下面的注解都会失效 |
@DisplayName | 无 | 相当于 TestNG 中的 description 属性,测试类/方法的显示名称 让测试结果显示更友好, |
@BeforeEach | @Before | 两个版本间是一一对应的,只是 JUnit 5 中的注解语义上更清晰 |
@BeforeAll | @BeforeClass | |
@AfterEach | @After | |
@AfterAll | @AfterClass | |
@Disabled | @Ignore | |
@Tag | @Category | JUnit 4 中的 @Category 是一个试验性的注解 |
@ExtendWith | @RunWith |
@DisplayName
显示测试类或方法的友好名称,目前在 IDEA 中有效,在 Maven 下没效果。例如下面的代码
1 2 3 4 5 6 |
@Test @DisplayName("1 plus 2 should be 3") public void onePlugTwoShouldBeThree() { Calculator calc = new Calculator(); assertEquals(3, calc.add(1, 2)); } |
Maven 测试正常的情况下本来就不显示测试方法名称,即使将来 Maven 中能显示 @DisplayName 中的名称,估计出错的时候还得显示实际方法名。
@Tag
它用来对测试进行分类,例如含有 @Tag("remote") 的测试用例在 Jenkins 上不应执行。但仅用字符串标识对书写时要求太高,有可能谁就写了一个 @Tag("Remote"). 一种办法就是用常量来代替字符串 remote
, 再就是用元注解
1 2 3 4 5 |
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Tag("remote") public @interface Remote { } |
然后在测试方法上就可直接用 @Remote 注解来代替 @Tag("remote") 的功效
1 2 3 4 5 |
@Test @Remote public void testGetUser() { //...... } |
进一步,可以定义包含了 @Tag 和 @Test 的元注解,那么测试用例可只用 @RemoteTest, 而不必 @Test 和 @Remote.
二. JUnit 5 断言
JUnit 5 的断言由 org.junit.jupiter.api.Assertitions 静态方法提供, 比以前稍微丰富些。无论是 JUnit 4 还是 JUnit 5 的 @Test 注解,断言方法是可以混用的。基本的断言方法没有多大变化,仍然是 assertEquals
会让人傻傻分不清哪个是期望值,哪个是实际值。所以我继续坚持用 AssertJ 提供的流畅的断言方式,类型推断,根本不用记忆某个类型相应的 Matcher。
JUnit 5 断言有几个改进之处
1) 错误消息可由 Supplier<String> 函数提供
JUnit 5 断言方法的错误消息既可以以直接字符串,也能用 Supplier<String> 函数提供。函数提供消息字符串的作用是可以延迟计算,只在断言失败时执行 Supplier<String> 函数
1 2 3 |
public static void assertEquals(float expected, float actual, Supplier<String> messageSupplier) { AssertEquals.assertEquals(expected, actual, messageSupplier); } |
1 2 3 4 5 |
//不管成功与否,都会进行后面的字符串操作 assertEquals(expected, actual, baseMessage + "wrong value " + actual); //只有在失败时才会进行后面的字符串操作 assertEquals(expected, actual, () -> baseMessage + "wrong value " + actual); |
这可以一定程度的提升测试代码的性能。
2) assertAll 接受多个 org.junit.jupiter.api.Executable 函数提供与条件断言
1 2 3 4 |
assertAll("Math", () -> assertEquals(2, 1 + 1), () -> assertTrue(1 > 0) ); |
与其说是方便,还不如说是罗嗦,怎么都没有 AssertJ
那种方法链的类 assertAll 操作方法
1 |
assertThat(actual).containsString("hello").containsString("world"); |
3) assertThrows 方法断言异常(值得关注)
1 2 3 4 5 6 |
@Test public void testDivide() { Calculator calc = new Calculator(); ArithmeticException exception = Assertions.assertThrows(ArithmeticException.class, () -> calc.divide(1, 0)); assertEquals(exception.getMessage(), "/ by zero"); } |
JUnit 5 的 @Test 注解不再支持 expected
属性,这是综合了先前 JUnit 版本的
@Test(expected = ArithmeticException.class) 和
@Rule public ExpectedException expectedEx= ExpectedException.none();
的改进的写法
三. JUnit 5 前置条件
这不是什么新东西,在 JUnit 4 中已有该特性,只是我之前未曾使用过。JUnit 4 中 assumeXxx() 方法在 org.junit.Assume
中,JUnit 5 的 assumeXxx() 方法在 org.junit.jupiter.api.Assumptions
中。前置条件用于决定是否要进行后续的测试,所以它与断言的区别是,前置条件不满足不会使得测试失败,而是忽略该测试,效果是与 @Ignore 或 @Disabled 是一致的,我们可以称之为有条件的 @Ignore 或 @Disabled。
同样要注意莫用 JUnit 5 的 @Test 与 JUnit 4 的 Assume 搭配使用,否则产生的是测试代码抛出 RuntimeException 异常那样的失败失败(不是断言失败),而非忽略,反之亦然。
四. 嵌套测试
这是个新鲜玩艺,可以让我们在测试类中嵌套定义测试类,并且嵌套层次不限,内部测试类中也可以用 @BeforeEach 和 @AfterEach 等注解。
在我们 JUnit 5 以前,以下代码是不受 JUnit 4 待见的,根本找不到要执行的测试用例
1 2 3 4 5 6 7 8 9 10 11 |
import org.junit.Test; public class CalculatorTest { class OperatorTest { @Test public void testFoo(){ } } } |
要求测试用例只能写在顶层类中。
JUnit 5 用 @Nested 注解使用测试类的嵌套成为了可能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; public class CalculatorTest { @Nested class OperatorTest { @Test public void testFoo(){ } } } |
IntelliJ IDEA 中的执行效果
Maven 执行测试时会统计到每一个具体的测试方法,并且失败时能准确定位到错误
嵌套测试提供了一种组织测试类的方式,不过我目前还未领悟到它的精髓所在。
五. 测试方法是一个独立生命周期
学习到这里才意识到我一直存在这么一个误区,以为 JUnit 是以测试类为一个生命周期的。比如在同一个类中多个测试方法使用了同一个实例变量的情况下,总会用一个 @After
方法来复位该实例变量,现在才知道那是多余的, 错误的使用了 @After
。关于 JUnit 以测试方法为生命周期可参考我的一篇日志 JUnit 中是以测试方法为一个独立的生命周期
JUnit 5 提供了方法来把生命周期由方法改为测试类,对于单个测试类可以直接使用注解 @TestInstance(Lifecycle.PER_CLASS)
, 默认是 PER_METHOD
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class CalculatorTest { @Test public void test1() { System.out.println(this); } @Test public void test2() { System.out.println(this); } } |
上面代码相当于把 JUnit 的执行行为由
1 2 |
new CalculatorTest().test1(); new CalculatorTest().test2(); |
变成了
1 2 3 |
CalculatorTest calculatorTest = new CalculatorTest(); calculatorTest.test1(); calculatorTest.test2(); |
这样前面两个测试用例输出的 this 就是同一个实例了,默认 PER_METHOD
时每个测试方法都属于各自不同的实例。
cc.unmi.CalculatorTest@39c0f4a
cc.unmi.CalculatorTest@39c0f4a
这时候,如果它们使用于实例变量就要特别注意用 @BeforeEach
或 @AfterEach
来复位。
另外,还可以全局修改 Lifecycle 为 PER_CLASS,两种方式
- 系统属性 -Djunit.jupiter.testinstance.lifecycle.default=per_class
- classpath 下属性文件 junit-platform.properties (如 src/test/resoures 下), 加上内容
junit.jupiter.testinstance.lifecycle.default = per_class
就是想不到有什么理由需要这样的全局默认行为的改动,这或会使得很多测试产生诡异的结果。
六. 依赖注入
在 JUnit 5 之前,标准的测试类不能有非默认构造函数,测试方法不允许有参数。只是说标准的情况下,我曾经用 JMockit 时通过测试方法来注入 Mock 对象。
测试类的构造函数,或 @Test, @TestFactory, @BeforeEach, @AfterEach, @BeforeAll, 或 @AfterAll 注解的方法都可以接受参数,参数由 ParameterResolver 来解析,它本身是一个扩展,实现了 Extension
接口,且目前有三个实现
- TestInfoParameterResolver
- TestReporterParameterResolver
- RepetionInfoParameterResolver
也就是说可接受的参数类型相应的为 TestInfo, TestReporter, RepetionInfo. 举个例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class CalculatorTest { public CalculatorTest(TestInfo testInfo) { System.out.println(testInfo.getTestClass().get()); } @Test @DisplayName("my test 1") public void test1(TestInfo testInfo) { System.out.println(testInfo.getDisplayName()); } @Test public void test2(TestReporter testReporter) { testReporter.publishEntry("a key", "a value"); } } |
输出如下
class cc.unmi.CalculatorTest
my test 1
class cc.unmi.CalculatorTest
timestamp = 2017-09-25T00:07:04.736, a key = a value
TestInfo 可以提供的信息有 DisplayName, Tags, TestClass, 和 TestMethod.
在 JUnit 5 之前可以用 TestName
这个 @Rule
来简单获取测试信息,JUnit 5 中没有了 @Rule
这个概念。
有些 ParameterResolver 由扩展 @ExtendWith
引入了,如 MockitExtension 本身就实现了 ParameterResolver 接口。可以用下面的方式注入 Mock 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@ExtendWith(MockitoExtension.class) public class MyMockitoTest { @BeforeEach public void init(@Mock Person person) { when(person.getName()).thenReturn("Dilbert"); } @Test public simpleTestWithInjectedMock(@Mock Person person) { assertEquals("Dilbert", person.getName()); } } |
七. 重复测试
可用 RepeatedTest(value = times, name = "{displayName} {currentRepetition} / {totalRepetitions}")
来定义重复次数,以及如何显示,name 为可选,可使用三个内置的变量。
那么重复测试的实际意义在哪里呢?
八. 参数化测试
这个功能在 JUnit 4 中就已存在,RunWith(Parameterized.class)
,主要是提供输入与结果数据不太方便,所以曾跃跃欲试而最终放弃。到了 JUnit 5 中参数化测试反而移入到了实验中的功能,看过 C# 的同事大量用参数化测试,想看看 JUnit 5 的这一功能进化成怎样了。
体验了一下好像没多大区别,不过还是让大家欣赏一下
首先需要引入库 org.junit.jupiter:junit-jupiter-params:5.0.0
, 代码是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class CalculatorTest { static Collection<Object[]> data() { return Arrays.asList(new Object[][]{ {1, 2, 3}, {2, 2, 4}, {2, 3, 6}, {3, 5, 8}, }); } @ParameterizedTest(name = "#{index}, {0} plus {1} should be {2}") @MethodSource(value = "data") public void testAddition(int x, int y, int z) { Calculator calculator = new Calculator(); assertEquals(z, calculator.add(x, y)); } } |
IntelliJ IDEA 下的效果
Maven 控制台下的效果
其他特性如 测试模板,动态测试,扩展模型。扩展模型算是 JUnit 5 的大头戏,所以这里不展开来细讲。
前头细数了那么 8 个所谓的新特性,仔细与 JUnit 4 对比了下好像也没几个新鲜的东西。
- 碰上一个 DisplayName 可以让测试报告变漂亮的,可是 Maven 却视而不见。
- 还有些像 嵌套测试,重复测试 没想到有什么大用处。
- Assertions 没多大起色,我仍然亲赖于 AssertJ
- Assumptions, 参数化测试 以前也有。依赖注入 在非标准的 JUnit 4 中也用过
- Tag 用于过滤测试应该比 @Category 用起来方便些,特别是借助于元注解。@Category 必须指定类,而 Tag 指定字符串就行。
- 说到头,还是扩展最令人期待(尚未深入)
参考:
本文链接 https://yanbin.blog/learn-junit-5-new-features/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。