
《JUnit in Action》全新第3版封面截图
写在前面
再次强调,这一章的重点是快速扫盲,因此知识点相对密集,但并未深入展开讨论。梳理本章知识点时我也只记录核心要点,只在个别实测过程中略作补充。现在 AI 工具如此便捷,对提到的 JUnit 特性如果有疑问,可以快速得到满意的答复。对于需要夯实基础的朋友来说,能逐一消化每个功能特性固然很好;如果条件不允许,至少也要建立印象,以便今后知道往什么方向查阅资料。
文章目录
-
- [2.5 @DisplayName 注解](#2.5 @DisplayName 注解)
- [2.6 @Nested 注解](#2.6 @Nested 注解)
- [2.7 @Tag 注解](#2.7 @Tag 注解)
- [2.8 断言方法](#2.8 断言方法)
- [2.9 新版超时断言](#2.9 新版超时断言)
- [2.10 需要抛出异常的断言测试](#2.10 需要抛出异常的断言测试)
- [2.11 假设断言](#2.11 假设断言)
(接上篇)
2.5 @DisplayName 注解
(详见 上一篇)
2.6 @Nested 注解
用于测试内部类中的待测试方法。
内部类的经典应用场景是通过 Builder
模式初始化一个类(强烈建议自行手动实现一遍 Customer
实体类,加深印象):
java
public class Customer {
private final Gender gender;
private final String firstName;
private final String lastName;
private final String middleName;
private final Date becomeCustomer;
public static class Builder {
private final Gender gender;
private final String lastName;
private final String firstName;
private String middleName;
private Date becomeCustomer;
public Builder(Gender gender, String firstName, String lastName) {
this.gender = gender;
this.firstName = firstName;
this.lastName = lastName;
}
public Builder withMiddleName(String middleName) {
this.middleName = middleName;
return this;
}
public Builder withBecomeCustomer(Date becomeCustomer) {
this.becomeCustomer = becomeCustomer;
return this;
}
public Customer build() {
return new Customer(this);
}
}
private Customer(Builder builder) {
this.gender = builder.gender;
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.middleName = builder.middleName;
this.becomeCustomer = builder.becomeCustomer;
}
// getters
}
@Nested
注解的用法(L5):
java
public class NestedTestsTest {
private static final String FIRST_NAME = "John";
private static final String LAST_NAME = "Smith";
@Nested()
class BuilderTest {
private final String MIDDLE_NAME = "Michael";
@Test
void customerBuilder() throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM-dd-yyyy");
Date customerDate = simpleDateFormat.parse("04-21-2019");
Customer customer = new Customer.Builder(Gender.MALE, FIRST_NAME, LAST_NAME)
.withMiddleName(MIDDLE_NAME)
.withBecomeCustomer(customerDate)
.build();
assertAll(() -> {
assertEquals(Gender.MALE, customer.getGender());
assertEquals(FIRST_NAME, customer.getFirstName());
assertEquals(LAST_NAME, customer.getLastName());
assertEquals(MIDDLE_NAME, customer.getMiddleName());
assertEquals(customerDate, customer.getBecomeCustomer());
});
}
}
}
这部分最吸引眼球的是 Builder 构建模式的手动实现,以及全新的断言方法 assertAll()
与 JDK 8
的 Lambda
表达式的结合。根据 assertAll
的签名,最后的断言逻辑还可以改写为如下模式,并且都能起到"执行所有断言、但不因某一个失败而中断后续断言的判定"的目的:
java
assertAll(
() -> assertEquals(Gender.MALE, customer.getGender()),
() -> assertEquals(FIRST_NAME, customer.getFirstName()),
() -> assertEquals(LAST_NAME, customer.getLastName()),
() -> assertEquals(MIDDLE_NAME, customer.getMiddleName()),
() -> assertEquals(customerDate, customer.getBecomeCustomer())
);
本地 IDEA
的实测效果如下(起到了很好的分组效果):

2.7 @Tag 注解
该注解是 JUnit 4
中 @Category
的升级版,可通过 IDE
或 pom.xml
配置,实现指定类别的测试类或测试方法的分组运行。
pom.xml
配置(推荐做法):
xml
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<groups>individual</groups>
<excludedGroups>repository</excludedGroups>
</configuration>
</plugin>
</build>
IDEA
配置:

原书截图界面和实测版本相差较大,新版 IDEA
已通过运行 配置文件 来完成相关设置。根据实测情况,Tags
标签可用 |
、&
等符号实现多个标签的组合运行(分别表示 或 、且 )。特别地,对于 且 的情况还有两种写法:
java
// version 1
@Tag("individual")
@Tag("repository")
public class CustomerTest {
// snip
}
// version 2
@Tags({@Tag("individual"), @Tag("repository")})
public class CustomerTest {
// snip
}
其中第二种的可读性更好。启用 @Tag
注解后,执行命令 mvn test
将只对指定了标签、且明确设置参与测试的单元测试用例才会最终执行。由于无法保证运行测试的人都使用 IDEA
,因此更推荐使用 pom.xml
来配置 Tag
标签。
2.8 断言方法
新版 JUnit 5
提供了大量的断言方法,并支持 Java 8
的函数式声明提高测试性能。常见的几种有:
断言方法 | 功能 |
---|---|
assertAll |
断言所有提供的可执行对象都不会抛出异常,参数类型为 org.junit.jupiter.api.function.Executable 型对象或对象数组。 |
assertArrayEquals |
断言预期数组与实际数组相等。 |
assertEquals |
断言期望值与实际值相等。 |
assertX(..., String message) |
当断言失败时,向测试框架提供指定消息的断言。 |
assertX(..., Supplier<String> msgSupplier) |
当断言失败时,向测试框架提供指定消息的断言。报错后的消息提示会通过 msgSupplier 延迟获取。 |
此外,JUnit 4
中的 assertThat
断言在新版中被移除,该断言由 JUnit
第三方辅助框架 Hamcrest
重新实现,更加灵活且符合 Java 8
特性。
关于
Hamcrest
框架该框架是辅助编写
JUnit
测试用例的第三方工具框架,内置了大量可读性极强的断言方法和辅助工具(各种matcher
匹配器)。其名称Hamcrest
就是matchers
各字母变位后的组合单词,以突出其灵活实用的断言特性。
2.9 新版超时断言
对于超时场景下的断言测试,JUnit 5
提供了两种超时机制:
- 超时后立即停止测试,不等待可执行的目标代码最终完成(使用
assertTimeout
断言); - 超时后继续执行测试,直到可执行的目标代码最终完成(使用
assertTimeoutPreemptively
断言);
java
class AssertTimeoutTest {
private SUT systemUnderTest = new SUT("Our system under test");
@Test
@DisplayName("A job is executed within a timeout")
void testTimeout() throws InterruptedException {
systemUnderTest.addJob(new Job("Job 1"));
assertTimeout(ofMillis(500), () -> systemUnderTest.run(200));
}
@Test
@DisplayName("A job is executed preemptively within a timeout")
void testTimeoutPreemptively() throws InterruptedException {
systemUnderTest.addJob(new Job("Job 1"));
assertTimeoutPreemptively(ofMillis(500), () -> systemUnderTest.run(200));
}
}
assertTimeout()
超时后的报错信息的句式为:execution exceeded timeout of 100 ms by 193 ms.
;
assertTimeoutPreemptively()
超时后的报错信息的句式为:execution timed out after 100 ms.
;
2.10 需要抛出异常的断言测试
JUnit 5
还对需要抛异常的应用场景提供了便捷的断言方法。既可以直接书写 assertThrows()
断言,也可以通过该断言返回的 Throwable
对象作进一步断言,例如断言异常原因是否为指定的内容等。
实测代码如下:
java
class AssertThrowsTest {
private SUT systemUnderTest = new SUT("Our system under test");
@Test
@DisplayName("An exception is expected")
void testExpectedException() {
assertThrows(NoJobException.class, systemUnderTest::run);
}
@Test
@DisplayName("An exception is caught")
void testCatchException() {
Throwable throwable = assertThrows(NoJobException.class, () -> systemUnderTest.run(1000));
assertEquals("No jobs on the execution list!", throwable.getMessage());
}
}
2.11 假设断言
应用场景:满足某种前提条件后,方可执行后续的断言测试;否则直接跳过该断言的执行。
示例代码:
java
class AssumptionsTest {
private static String EXPECTED_JAVA_VERSION = "1.8";
private TestsEnvironment environment = new TestsEnvironment(
new JavaSpecification(System.getProperty("java.vm.specification.version")),
new OperationSystem(System.getProperty("os.name"), System.getProperty("os.arch"))
);
private SUT systemUnderTest = new SUT();
@BeforeEach
void setUp() {
assumeTrue(environment.isWindows());
}
@Test
void testNoJobToRun() {
assumingThat(
() -> environment.getJavaVersion().equals(EXPECTED_JAVA_VERSION),
() -> assertFalse(systemUnderTest.hasJobToRun()));
}
@Test
void testJobToRun() {
assumeTrue(environment.isAmd64Architecture());
systemUnderTest.run(new Job());
assertTrue(systemUnderTest.hasJobToRun());
}
}
其中,L12
、L18
、L24
均为假设断言,如果该行假设不成立,则后续断言均不会执行。
(未完待续)