1. 概述
本文将探讨属性测试(Property Testing)的概念及其在 vavr-test 库中的实现。属性测试允许我们通过定义程序必须遵守的不变式(invariants)来描述其高层行为。
2. 什么是属性测试?
属性是不变式与输入值生成器的组合。对于每个生成的值,不变式会作为谓词进行验证,判断该值是否满足条件。
一旦出现使不变式为假的值,属性即被证伪(falsified),测试立即终止。若在指定数量的样本数据后属性仍未被证伪,则认为该属性满足条件。
这种机制确保测试在条件不满足时快速失败,避免执行不必要的检查。
3. Maven 依赖
首先添加 vavr-test 库的 Maven 依赖:
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr-test</artifactId>
<version>${vavr.test.version}</version>
</dependency>
<properties>
<vavr.test.version>2.0.5</vavr.test.version>
</properties>
⚠️ 注意:原文中的
artifactId
存在笔误(jvavr-test
),已修正为正确值vavr-test
4. 编写属性测试
考虑一个返回字符串流的函数。这是一个从 0 开始的无限流,通过简单规则将数字映射为字符串。这里使用了 Vavr 的模式匹配(Pattern Matching)特性:
private static Predicate<Integer> divisibleByTwo = i -> i % 2 == 0;
private static Predicate<Integer> divisibleByFive = i -> i % 5 == 0;
private Stream<String> stringsSupplier() {
return Stream.from(0).map(i -> Match(i).of(
Case($(divisibleByFive.and(divisibleByTwo)), "DividedByTwoAndFiveWithoutRemainder"),
Case($(divisibleByFive), "DividedByFiveWithoutRemainder"),
Case($(divisibleByTwo), "DividedByTwoWithoutRemainder"),
Case($(), "")));
}
为这类方法编写单元测试容易出错,因为很可能遗漏边界条件,无法覆盖所有场景。幸运的是,我们可以用属性测试自动覆盖所有边界情况。
4.1 定义输入生成器
首先定义测试所需的输入类型:
Arbitrary<Integer> multiplesOf2 = Arbitrary.integer()
.filter(i -> i > 0)
.filter(i -> i % 2 == 0 && i % 5 != 0);
这里指定输入必须满足两个条件:
- 大于零
- 能被 2 整除但不能被 5 整除
4.2 定义验证条件
接下来定义验证函数,检查被测方法对给定参数是否返回正确值:
CheckedFunction1<Integer, Boolean> mustEquals
= i -> stringsSupplier().get(i).equals("DividedByTwoWithoutRemainder");
4.3 执行属性测试
使用 Property
类启动测试:
CheckResult result = Property
.def("Every second element must equal to DividedByTwoWithoutRemainder")
.forAll(multiplesOf2)
.suchThat(mustEquals)
.check(10_000, 100);
result.assertIsSatisfied();
关键参数说明:
forAll(multiplesOf2)
:对所有满足条件的整数suchThat(mustEquals)
:必须满足验证条件check(10_000, 100)
:生成 10,000 个输入值,运行 100 次测试
4.4 扩展测试用例
快速编写另一个测试:验证当输入能被 2 和 5 整除时,函数应返回 "DividedByTwoAndFiveWithoutRemainder"
。
调整生成器和验证函数:
Arbitrary<Integer> multiplesOf5 = Arbitrary.integer()
.filter(i -> i > 0)
.filter(i -> i % 5 == 0 && i % 2 == 0);
CheckedFunction1<Integer, Boolean> mustEquals
= i -> stringsSupplier().get(i).endsWith("DividedByTwoAndFiveWithoutRemainder");
执行 1,000 次迭代测试:
Property.def("Every fifth element must equal to DividedByTwoAndFiveWithoutRemainder")
.forAll(multiplesOf5)
.suchThat(mustEquals)
.check(10_000, 1_000)
.assertIsSatisfied();
5. 总结
本文快速介绍了属性测试的核心概念。我们使用 vavr-test 库创建了测试,通过 Arbitrary
、CheckedFunction
和 Property
类实现属性测试。
所有示例代码可在 GitHub 项目 中获取(Maven 项目,可直接导入运行)。
关键优势
✅ 自动覆盖边界条件
✅ 快速失败机制
✅ 减少手工测试用例编写负担
✅ 适合复杂逻辑验证
适用场景
- 复杂算法验证
- 数据转换逻辑测试
- 状态机行为检查
- 需要覆盖大量输入组合的情况