之前介绍了下JUnit 5的架构和如何在maven下运行JUnit 5测试。这篇博客主要介绍下JUnit 5的新特性。
在JUnit 4里我们的测试方法必须定义为public的访问级别,如果没有定义成public,虽然编译的时候不会提示异常,但是在运行时会提示 “java.lang.Exception: Method testIsBlank() should be public” 如下错误信息。
而在JUnit 5里,我们不再需要将测试类与测试方法定义为public了,默认的包可见的访问级别就可以了。
那就能偷懒少敲几个单词了哈哈!
对于在JUnit 4中的常用注解,你都可以在JUnit 5中找到对应的注解,关系如下:
JUnit 5 | JUnit 4 | 说明 |
---|---|---|
@Test | @Test | 指明被注解的方法是一个测试方法,注意JUnit 5的@Test在jupiter-api里 |
@BeforeAll | @BeforeClass | 被注解的静态方法会在当前类的所有@Test方法执行前执行一次 |
@BeforeEach | @Before | 被注解的方法会在当前类的每个@Test方法执行前执行一次 |
@AfterAll | @AfterClass | 被注解的静态方法会在当前类的所有@Test方法执行后执行一次 |
@AfterEach | @After | 被注解的方法会在当前类的每个@Test方法执行后执行一次 |
@Disabled | @Ignore | 被注解的方法不会被执行,但是在测试报告里会记录为已执行 |
public class JUnit5Test {
@BeforeAll
public static void beforeAll() {
System.out.println("在所有test方法执行前执行 @BeforeAll注解方法");
}
@AfterAll
public static void afterAll() {
System.out.println("在所有test方法执行后执行 @AfterAll注解方法");
}
@BeforeEach
public void beforeEachTest() throws Exception {
System.out.println("在每个test方法执行前执行 @BeforeEach注解方法");
}
@AfterEach
public void afterEachTest() throws Exception {
System.out.println("在每个test方法执行后执行 @AfterEach注解方法");
}
}
写测试用例的时候,为了更好的可读性,我们往往会给测试方法定义一个有意义的名字,eg.testXXXWhenXXXThenReturnXXX。JUnit 5还提供了一个@DisplayName 注解,方便我们为每个测试用例添加更具体的名字,更容易表述用例所要测试的内容(可以是字符串,特殊符号,甚至是表情符号)。
public class DisplayNameTest {
@Test
@DisplayName("执行aTest方法")
void aTest() {
assertEquals(4, (2 + 2));
}
}
测试用例中断言可以算是最重要的一部分了,没有断言的测试是不完整的。
junit 框架中最常用的断言就是检查一个对象或者属性是否为null.或者判断两个属性是否一致。JUnit 4和JUnit 5中的断言方法都可以接受字符串作为一个可选参数,如果断言失败,则会在控制台输出对应的描述信息。JUnit 5中还可以使用 lambda 表达式 来构建这个描述信息。
注意一下,在JUnit 4和JUnit 5中描述信息的参数位置是不一样的。
@Test
@DisplayName("assert all test")
void assertTest() {
String expected = "chenpp";
String actual = "chen"+ "pp";
String nullValue = null;
assertEquals(expected, actual, "incorrect!");
assertNull(nullValue);
}
assertAll
判断一组断言是否满足,包含的所有断言都会执行,即使其中一个或多个断言失败
如下代码中第一个和第四个断言都是错误的,在执行的时候会把5个断言都执行一遍,而不像常规断言一样,遇到错误的断言就停止运行
@Test
void assertAllTestWhenOneIsError() {
String expected = "Hi,chenpp";
String actual = "Hi," + "chenpp";
String nullValue = null;
assertAll(
"Assert All of these",
() -> assertEquals(expected, "", "incorrect!"),
() -> assertFalse(nullValue != null),
() -> assertNull(nullValue),
() -> assertNull("not null", "incorrect!"),
() -> assertTrue(nullValue == null));
}
这样我们在开发单元测试的时候就可以一次把所有的错误信息都打印出来,而不会在测试第一个断言失败时就结束执行,这样只能得到第一个出错地方的得到提示,也无法得知其他断言是否成功,只能再跑一遍测试。
assertThrows和 expectThrows
都用于判断是否抛出期望的异常类型,在JUnit 4 中是通过 expected = 方法参数或者 @Rule 提供此能力。JUnit 5的expectThrows还会返回抛出的异常实例,用于进一步的验证
@Test
public void exception() {
assertThrows(ArithmeticException.class, () -> System.out.println(1 / 0));
}
JUnit 5新增了@Tag注解,可以为测试类或方法添加标签,并在执行时快速地根据标签来针对性地运行测试。
使用maven运行测试用例时,使用如下参数指定tag名称即可
-Dgroups=“tagName”
@Tag("test")
public class TagTest {
@Test
@Tag("test1")
void test1() {
assertEquals(2, 1 + 1);
}
@Test
@Tag("test2")
void test2() {
assertEquals(2, 1 + 1);
}
}
通过@Tag可以为测试类或方法添加标签,但是不同的标签只是通过字符串来进行区分,并不是类型安全的。开发过程中如果拼写错误就可能导致标签没有被正确应用。更好的做法是使用类型安全的元注解(meta annotation),如下
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("history")
public @interface History{
}
使用@History的作用等同于@Tag(“history”),这样不仅提高了代码的可读性,也可以避免拼写错误可能带来的问题。
在JUnit 5出来之前,我们如果想对JUnit 4 的核心功能进行扩展,往往都会使用自定义Runner 和 @Rule。
自定义Runner 通常是 BlockJUnit4ClassRunner 的子类,用于实现 JUnit 中没有直接提供的某种功能。 eg.spring-test框架的SpringJUnit4ClassRunner, 和mock框架的MockitoJUnitRunner
局限性:
JUnit 5扩展机制的核心准则:
Prefer extension points over features
基于这一准则,JUnit 5 中定义了许多扩展点,每个扩展点都对应一个接口。我们可以定义自己的扩展可以实现其中的某些接口,然后通过 @ExtendWith 注解注册给 JUnit,后者会在特定的时间点调用注册的接口实现。
JUnit 5定义的部分扩展点:
接口 | 说明 |
---|---|
BeforeAllCallback | 在所有测试方法执行前定义测试容器执行的行为,也就是在 @BeforeAll 注解的方法之前执行 |
AfterAllCallback | 在@AfterAll 注解的方法之后执行 |
BeforeEachCallback | 在@BeforeEach 注解的方法之前执行 |
AfterEachCallback | 在@AfterEach 注解的方法之后执行 |
BeforeTestExecutionCallback | 在测试方法运行之前立刻执行 |
AfterTestExecutionCallback | 在测试方法运行之后立刻执行 |
ParameterResolver | 用于在运行时动态解析参数 |
Extension Context
Extension Context包含了测试方法的上下文信息,所有的扩展接口都定义了一个包含该参数的接口方法,开发者可以根据Extension Context拿到跟测试方法相关的几乎所有的信息,包括方法注解,测试实例,测试标签等。
Store
Extension Context 提供了一个getStore(Namespace namespace)的接口方法,返回了一个Store的对象,用于存储一些数据,方便不同的回调接口之间共享数据。
Namespace
如果你需要使用Extension Context里的Store功能,那么你就需要先申请一个Namespace,它可以避免不同的扩展实现误操作同一份数据
比方说,现在我们想实现一个功能–计算每个测试方法的执行时间。为了方便使用,我们定义一个注解@CalTime,如果想计算测试方法的执行时间,就在该方法上加上@CalTime注解即可
@Target({ TYPE, METHOD, ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(CalTestTimeExtension.class)
public @interface CalTime {
}
根据JUnit 5的扩展点,我们的CalTestTimeExtension需要实现BeforeTestExecutionCallback和AfterTestExecutionCallback 两个扩展API
public class CalTestTimeExtensionimplements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create("CalTestTimeExtension");
private static final String STORE_KEY = "calTime";
@Override
public void beforeTestExecution(ExtensionContext context) {
//判断是否有添加对应的注解
if (!isSupport(context)) {
return;
}
context.getStore(NAMESPACE).put(STORE_KEY, System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) {
if (!isSupport(context))
return;
long beginTime = (long) context.getStore(NAMESPACE).get(STORE_KEY);
long executeTime = System.currentTimeMillis() - beginTime;
System.out.println("Test " + context.getDisplayName() +" execute cost " + executeTime + " ms");
}
private static boolean isSupport(ExtensionContext context) {
return context.getElement()
.map(el -> el.isAnnotationPresent(CalTime.class))
.orElse(false);
}
}
参考资料:
「译」JUnit 5 系列:基础入门
Junit5 用户指南