【JUnit实战3_03】第二章:探索 JUnit 的核心功能(二)
《JUnit in Action》全新第3版封面截图
写在前面
再次强调,这一章的重点是快速扫盲,因此知识点相对密集,但并未深入展开讨论。梳理本章知识点时我也只记录核心要点,只在个别实测过程中略作补充。现在 AI 工具如此便捷,对提到的 JUnit 特性如果有疑问,可以快速得到满意的答复。对于需要夯实基础的朋友来说,能逐一消化每个功能特性固然很好;如果条件不允许,至少也要建立印象,以便今后知道往什么方向查阅资料。
文章目录
- 2.5 @DisplayName 注解
- 2.6 @Nested 注解
- 2.7 @Tag 注解
- 2.8 断言方法
- 2.9 新版超时断言
- 2.10 需要抛出异常的断言测试
- 2.11 假设断言
2.5 @DisplayName 注解
(详见 上一篇)
2.6 @Nested 注解
用于测试内部类中的待测试方法。
内部类的经典应用场景是通过 Builder
模式初始化一个类(强烈建议自行手动实现一遍 Customer
实体类,加深印象):
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):
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";@Testvoid 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
的签名,最后的断言逻辑还可以改写为如下模式,并且都能起到“执行所有断言、但不因某一个失败而中断后续断言的判定”的目的:
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
配置(推荐做法):
<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
标签可用 |
、&
等符号实现多个标签的组合运行(分别表示 或、且)。特别地,对于 且 的情况还有两种写法:
// 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
断言);
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
对象作进一步断言,例如断言异常原因是否为指定的内容等。
实测代码如下:
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 假设断言
应用场景:满足某种前提条件后,方可执行后续的断言测试;否则直接跳过该断言的执行。
示例代码:
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();@BeforeEachvoid setUp() {assumeTrue(environment.isWindows());}@Testvoid testNoJobToRun() {assumingThat(() -> environment.getJavaVersion().equals(EXPECTED_JAVA_VERSION),() -> assertFalse(systemUnderTest.hasJobToRun()));}@Testvoid testJobToRun() {assumeTrue(environment.isAmd64Architecture());systemUnderTest.run(new Job());assertTrue(systemUnderTest.hasJobToRun());}
}
其中,L12
、L18
、L24
均为假设断言,如果该行假设不成立,则后续断言均不会执行。
(未完待续)