
《JUnit in Action》全新第3版封面截图
写在前面
作为第二章 JUnit 功能特性扫盲篇的最后一篇自学笔记,我个人对这部分的定位是增长见识、拓宽眼界为主。很多知识点都给人眼前一亮的感觉:原来 JUnit 5 还能这样用!但是时间精力确实有限,不可能完全展开讨论,留下关键印象就显得特别重要了。诀窍在于,记牢典型案例的应用场景。
文章目录
- 2.13 基于手动硬编码的字符串数组的参数化测试
- 2.14 基于枚举类的参数化测试
- 2.15 基于 CSV 文件的参数化测试
- 2.16 动态测试
- 2.17 Hamcrest 框架用法示例
2.13 基于手动硬编码的字符串数组的参数化测试
通过组合使用 @ParameterizedTest 注解和 @ValueSource 注解,可以自定义参数化测试用例的展示名称(默认将 strings 数组中的元素值作为各测试用例的子标题):
class ParameterizedWithValueSourceTest {
private WordCounter wordCounter = new WordCounter();
@ParameterizedTest
@ValueSource(strings = {"Check three parameters", "JUnit in Action"})
void testWordsInSentence(String sentence) {
assertEquals(3, wordCounter.countWords(sentence));
}
}
实测结果:

2.14 基于枚举类的参数化测试
通过组合使用 @ParameterizedTest 注解和 @EnumSource 注解,可以自定义参数化测试用例的展示名称(能在枚举注解内设置筛选条件,这个特性让人眼前一亮):
import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE;
class ParameterizedWithEnumSourceTest {
private WordCounter wordCounter = new WordCounter();
@ParameterizedTest
@EnumSource(Sentences.class)
void testWordsInSentence(Sentences sentence) {
assertEquals(3, wordCounter.countWords(sentence.value()));
}
@ParameterizedTest
@EnumSource(value = Sentences.class, names = {"JUNIT_IN_ACTION", "THREE_PARAMETERS"})
void testSelectedWordsInSentence(Sentences sentence) {
assertEquals(3, wordCounter.countWords(sentence.value()));
}
@ParameterizedTest
@EnumSource(value = Sentences.class, mode = EXCLUDE, names = {"THREE_PARAMETERS"})
void testExcludedWordsInSentence(Sentences sentence) {
assertEquals(3, wordCounter.countWords(sentence.value()));
}
enum Sentences {
JUNIT_IN_ACTION("JUnit in Action"),
SOME_PARAMETERS("Check some parameters"),
THREE_PARAMETERS("Check three parameters");
private final String sentence;
Sentences(String sentence) {
this.sentence = sentence;
}
public String value() {
return sentence;
}
}
}
实测结果:

2.15 基于 CSV 文件的参数化测试
最让人意外的是 JUnit 5 还能通过 @CsvFileSource 注解直接解析 CSV 文件,并将解析结果以 方法参数的形式 注入参数化测试(例如示例代码中的 expected 和 sentence)。如果数据量较小,也可以用 @CsvSource 注解手动输入 CSV 格式的数据源。
这部分内容其实和上一篇的参数注入有所重叠,为了强调 JUnit 在文件解析方面的强大,这里单列出来以便日后复盘。查看 @CsvFileSource 注解的源码还可以了解更多配置项,如分隔符的设置等,这里就不展开了。
示例代码1:
class ParameterizedWithCsvFileSourceTest {
private WordCounter wordCounter = new WordCounter();
@ParameterizedTest
@CsvFileSource(resources = "/word_counter.csv")
void testWordsInSentence(int expected, String sentence) {
assertEquals(expected, wordCounter.countWords(sentence));
}
}
上述代码中,CSV 文件路径是相对测试的 CLASSPATH 而言的,即 src/test/resources/。运行结果:

示例代码2:(手动录入数据源)
class ParameterizedWithCsvSourceTest {
private final WordCounter wordCounter = new WordCounter();
@ParameterizedTest(name = "Line {index}: [{0}] - {1}")
@CsvSource(value = {"2, Unit testing", "3, JUnit in Action", "4, Write solid Java code"})
@DisplayName(value = "should parse CSV file")
void testWordsInSentence(int expected, String sentence) {
assertEquals(expected, wordCounter.countWords(sentence));
}
}
运行结果:

2.16 动态测试
利用工厂模式注解 @TestFactory 可以动态生成多个测试用例。需要注意的是,最核心的测试方法需要返回如下指定类型:
- 一个
DynamicNode型对象(DynamicNode为抽象类,DynamicContainer和DynamicTest是其具体的实现类); - 一个
DynamicNode型数组; - 一个基于
DynamicNode的Stream流; - 一个基于
DynamicNode的Collection集合; - 一个基于
DynamicNode的Iterable可迭代对象; - 一个基于
DynamicNode的Iterator迭代器对象。
示例代码如下:
class DynamicTestsTest {
private PositiveNumberPredicate predicate = new PositiveNumberPredicate();
@BeforeAll
static void setUpClass() {
System.out.println("@BeforeAll method");
}
@AfterAll
static void tearDownClass() {
System.out.println("@AfterAll method");
}
@BeforeEach
void setUp() {
System.out.println("@BeforeEach method");
}
@AfterEach
void tearDown() {
System.out.println("@AfterEach method");
}
@TestFactory
Iterator<DynamicTest> positiveNumberPredicateTestCases() {return asList(dynamicTest("negative number", () -> {System.out.println("negative number ...");assertFalse(predicate.check(-1));}),dynamicTest("zero", () -> {System.out.println("zero ...");assertFalse(predicate.check(0));}),dynamicTest("positive number", () -> {System.out.println("positive number ...");assertTrue(predicate.check(1));})).iterator();}}
实测结果:

可以看到,各生命周期注解仅对添加了 @TestFactory 工厂注解的外层方法本身生效,对工厂方法中动态生成的测试用例均无效。具体的测试用例行为,由动态测试的第二个参数,即传入的 Executable 型断言对象决定。
备忘:意外发现一个 IDEA 控制台输出的 Bug
实测发现,
IntelliJ IDEA中的控制台输出结果与期望的顺序不符,每次在IDEA中运行动态测试,@BeforeAll和@AfterAll注解的输出结果都在最末尾:### snip ### @BeforeEach method negative number ... zero ... positive number ... @AfterEach method @BeforeAll method @AfterAll method Process finished with exit code 0但在命令行中的就是正确的顺序:
> mvn test -Dtest=DynamicTestsTest ### snip ### [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.manning.junitbook.ch02.dynamic.DynamicTestsTest @BeforeAll method @BeforeEach method negative number ... zero ... positive number ... @AfterEach method @AfterAll method [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.016 s - in com.manning.junitbook.ch02.dynamic.DynamicTestsTest ### snip ###
2.17 Hamcrest 框架用法示例
Hamcrest 辅助框架提供了更具声明式风格的测试断言方法和组合工具,可以让代码可读性更好,同时报错信息的提示更加友好。其中会大量涉及 Matcher 对象的组合应用(matcher 又称为 约束(constraints) 或 判定条件(predicates),相关概念源自 Java、C++、Objective-C、Python、PHP 等编程语言)。
对函数式编程感兴趣的朋友不妨多看看 Hamcrest 的源码,学习学习当中定义的各种辅助 Matcher 是如何构建一个相对完善的测试语义的。毕竟 Java 8 的函数式特性已经发布十余年了,我本人也在各类项目中有意尝试这些写法,但在构筑流畅的语义抽象层时经常遭遇巨大阻力,以致于很多项目后期运维难以为继,可读性和命名上的一致性都不强。究其原因,一是自身的英语素养还有待加强,二是可供参考的体例不多,三是社区对函数式编程的响应积极性并不高,尤其是在绝大多数中小公司都将代码重构当成增加项目综合成本的一大来源,这方面的刻意练习就更少了。
要启用 Hamcrest 也不难,先新增必要的 Maven 依赖:
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>2.1</version>
<scope>test</scope>
</dependency>
下面通过常规报错提示和 Hamcrest 提示的效果对比来演示具体用法(报错信息演示):
public class HamcrestListTest {
private List<String> values;@BeforeEachpublic void setUp() {values = new ArrayList<>();values.add("John");values.add("Michael");values.add("Edwin");}@Test@DisplayName("List without Hamcrest will intentionally fail to show how failing information is displayed")public void testListWithoutHamcrest() {assertEquals(3, values.size());assertTrue(values.contains("Oliver") || values.contains("Jack") || values.contains("Harry"));}@Test@DisplayName("List with Hamcrest will intentionally fail to show how failing information is displayed")public void testListWithHamcrest() {assertThat(values, hasSize(3));assertThat(values, hasItem(anyOf(equalTo("Oliver"),equalTo("Jack"),equalTo("Harry"))));assertThat("The list doesn't contain all the expected objects, in order",values,contains("Oliver", "Jack", "Harry"));assertThat("The list doesn't contain all the expected objects",values,containsInAnyOrder("Jack", "Harry", "Oliver"));}}
运行结果:

常见的 Hamcrest 静态工厂方法:
| 工厂方法 | 功能 |
|---|---|
anything | 匹配任意内容,常用于单纯提高可读性。 |
is | 仅用于提高语句的可读性 |
allof | 检查其中的条件是否都满足 |
anyOf | 检查包含的条件是否存在任意一个满足 |
not | 反转目标条件的语义 |
instanceOf | 检查某对象是否均为另一对象的实例 |
sameInstance | 测试对象的同一性 |
nullValue、notNullValue | 测试空值、非空值 |
hasProperty | 测试该 Java Bean 是否具有某个属性 |
hasEntry、hasKey、hasValue | 测试目标 Map 是否包含指定的项、键或值 |
hasItem、hasItems | 检测目标集合中是否存在某个或某些子项 |
closeTo、greaterThan、greaterThanOrEqualTo、lessThan、lessThanOrEqualTo | 测试目标数值是否接近、 大于、 大于或等于、 小于、 小于或等于某个值 |
equalToIgnoringCase | 测试某字符串是否与另一字符串相等(忽略大小写) |
equalToIgnoringWhiteSpace | 测试某字符串是否与另一字符串相等(忽略空白字符) |
containsString、endsWith、startsWith | 测试某字符串是否包含指定字符串、或者以指定字符串开头或结尾 |
不难看到,这些声明式的工厂方法都是非常简洁有力的,几乎不需要额外的注释。或许这才是函数式编程的正确打开方式吧。
(第二章完)