
本文深入探讨了jqwik中`arbitrary`的组合与复用策略,旨在帮助开发者高效生成复杂测试数据。文章首先纠正了`@forall`注解在`@provide`方法和领域中的使用误区,随后详细介绍了在不同场景下共享`arbitrary`的几种方法,包括静态函数、基于类型解析的自定义值类型,以及通过自定义注解实现更精细化的数据生成。通过丰富的示例代码,本文为构建可维护、可扩展的属性测试提供了实践指导。
在属性测试中,我们经常需要为复杂对象生成测试数据。jqwik提供了强大的Arbitrary机制来定义数据生成逻辑。例如,我们可能有一个包含多个字符串字段的复杂类MyComplexClass:
public class MyComplexClass {
private final String id; // 正整数形式
private final String recordId; // UUID形式
private final String creatorId; // 正整数形式
private final String editorId; // 正整数形式
private final String nonce; // UUID形式
private final String payload; // 随机字符串
// 假设存在一个Builder模式或全参数构造函数
public static Builder newBuilder() { return new Builder(); }
public static class Builder {
private String id;
private String recordId;
private String creatorId;
private String editorId;
private String nonce;
private String payload;
public Builder setId(String id) { this.id = id; return this; }
public Builder setRecordId(String recordId) { this.recordId = recordId; return this; }
public Builder setCreatorId(String creatorId) { this.creatorId = creatorId; return this; }
public Builder setEditorId(String editorId) { this.editorId = editorId; return this; }
public Builder setNonce(String nonce) { this.nonce = nonce; return this; }
public Builder setPayload(String payload) { this.payload = payload; return this; }
public MyComplexClass build() {
return new MyComplexClass(id, recordId, creatorId, editorId, nonce, payload);
}
}
private MyComplexClass(String id, String recordId, String creatorId, String editorId, String nonce, String payload) {
this.id = id;
this.recordId = recordId;
this.creatorId = creatorId;
this.editorId = editorId;
this.nonce = nonce;
this.payload = payload;
}
// Getter方法省略
@Override
public String toString() {
return "MyComplexClass{" +
"id='" + id + '\'' +
", recordId='" + recordId + '\'' +
", creatorId='" + creatorId + '\'' +
", editorId='" + editorId + '\'' +
", nonce='" + nonce + '\'' +
", payload='" + payload + '\'' +
'}';
}
}为了生成MyComplexClass的实例,我们首先需要定义其各个字段的Arbitrary。例如,可以创建生成UUID风格字符串和正整数风格字符串的Arbitrary
import net.jqwik.api.*;
import net.jqwik.api.arbitraries.StringArbitrary;
import net.jqwik.api.domains.DomainContextBase;
import java.util.Set;
import java.util.UUID;
public class MyArbitraries {
public static Arbitrary<String> arbUuidString() {
return Combinators.combine(
Arbitraries.longs(), Arbitraries.longs(), Arbitraries.of(Set.of('8', '9', 'a', 'b')))
.as((l1, l2, y) -> {
// 模拟UUID V4的格式,第14位为'4',第19位为'8','9','a','b'之一
StringBuilder b = new StringBuilder(new UUID(l1, l2).toString());
b.setCharAt(14, '4');
b.setCharAt(19, y);
return b.toString(); // 返回String,而不是UUID对象
});
}
public static Arbitrary<String> arbNumericIdString() {
// 生成非负短整数,并转换为字符串
return Arbitraries.shorts().map(Math::abs).map(i -> "" + i);
}
}然后,我们可以使用Builders.withBuilder来组合这些基础Arbitrary以生成MyComplexClass:
// 假设MyArbitraries是可访问的
public class MyComplexClassDomain extends DomainContextBase {
@Provide
public Arbitrary<MyComplexClass> arbMyComplexClass() {
return Builders.withBuilder(MyComplexClass::newBuilder)
.use(MyArbitraries.arbNumericIdString()).in(MyComplexClass.Builder::setId)
.use(MyArbitraries.arbUuidString()).in(MyComplexClass.Builder::setRecordId)
.use(MyArbitraries.arbNumericIdString()).in(MyComplexClass.Builder::setCreatorId)
.use(MyArbitraries.arbNumericIdString()).in(MyComplexClass.Builder::setEditorId)
.use(MyArbitraries.arbUuidString()).in(MyComplexClass.Builder::setNonce)
.use(Arbitraries.strings().alpha().ofLength(10, 20)).in(MyComplexClass.Builder::setPayload)
.build(MyComplexClass.Builder::build);
}
}在jqwik中,@ForAll注解不仅仅局限于@Property测试方法。它同样可以在@Provide注解的方法以及@Domain中发挥作用,允许Arbitrary的提供者方法之间相互依赖,从而构建更复杂的生成逻辑。
需要注意的是,@ForAll("name")中的字符串引用是局部解析的,仅限于当前类、其父类和包含类。这是为了避免全局字符串引用可能带来的歧义和维护困难。
以下是一个示例,展示了@Provide方法如何通过@ForAll相互协作:
import net.jqwik.api.*;
import net.jqwik.api.domains.Domain;
import net.jqwik.api.domains.DomainContextBase;
class MyDomain extends DomainContextBase {
// 提供一个Arbitrary<String>,其长度由另一个@Provide方法"lengths"提供
@Provide
public Arbitrary<String> strings(@ForAll("lengths") int length) {
return Arbitraries.strings().alpha().ofLength(length);
}
// 提供一个Arbitrary<Integer>用于生成字符串长度
@Provide
public Arbitrary<Integer> lengths() {
return Arbitraries.integers().between(3, 10);
}
// 此方法不会被strings()方法使用,因为strings()明确引用了"lengths"
@Provide
public Arbitrary<Integer> negatives() {
return Arbitraries.integers().between(-100, -10);
}
}
class MyProperties {
@Property(tries = 5)
@Domain(MyDomain.class) // 指定使用MyDomain
public void printOutAlphaStringsWithLength3to10(@ForAll String stringsFromDomain) {
// stringsFromDomain将由MyDomain中的strings()方法提供,其长度介于3到10之间
System.out.println("Generated String: " + stringsFromDomain + ", Length: " + stringsFromDomain.length());
}
}当我们需要在不同的测试类或领域中复用Arbitrary定义时,有几种不同的策略可以选择。
在单个领域内,或者当多个相关领域继承自同一个父类时,将Arbitrary定义为静态方法并直接调用是一种简单有效的共享方式。
优点:
缺点:
在第1节的MyArbitraries示例中,arbUuidString()和arbNumericIdString()就是静态函数,可以在MyComplexClassDomain中直接调用。
// MyArbitraries类保持不变,提供静态Arbitrary方法
// MyComplexClassDomain中直接调用静态方法
public class MyComplexClassDomain extends DomainContextBase {
@Provide
public Arbitrary<MyComplexClass> arbMyComplexClass() {
return Builders.withBuilder(MyComplexClass::newBuilder)
.use(MyArbitraries.arbNumericIdString()).in(MyComplexClass.Builder::setId)
.use(MyArbitraries.arbUuidString()).in(MyComplexClass.Builder::setRecordId)
// ... 其他字段
.build(MyComplexClass.Builder::build);
}
}当需要在不相关的领域之间共享Arbitrary,并且这些Arbitrary生成的数据具有特定的业务含义时,引入自定义值类型(Value Type)是一种推荐的实践。通过为特定类型的字符串或整数创建包装类,jqwik可以根据类型自动解析并提供相应的Arbitrary。
优点:
缺点:
示例:
定义值类型:
// 定义表示特定业务含义的字符串类型
public record MyId(String value) {}
public record MyRecordId(String value) {}
public record MyCreatorId(String value) {}
// ... 其他需要区分的类型在领域中提供这些值类型对应的Arbitrary:
public class MyDomainWithTypes extends DomainContextBase {
@Provide
public Arbitrary<MyId> idArbitrary() {
return MyArbitraries.arbNumericIdString().map(MyId::new);
}
@Provide
public Arbitrary<MyRecordId> recordIdArbitrary() {
return MyArbitraries.arbUuidString().map(MyRecordId::new);
}
@Provide
public Arbitrary<MyCreatorId> creatorIdArbitrary() {
return MyArbitraries.arbNumericIdString().map(MyCreatorId::new);
}
@Provide
public Arbitrary<MyComplexClass> arbMyComplexClassWithTypeFields() {
return Builders.withBuilder(MyComplexClass::newBuilder)
// jqwik会根据类型自动找到对应的Arbitrary,然后通过lambda进行转换
.use(Arbitraries.defaultFor(MyId.class)).in((builder, id) -> builder.setId(id.value()))
.use(Arbitraries.defaultFor(MyRecordId.class)).in((builder, recordId) -> builder.setRecordId(recordId.value()))
.use(Arbitraries.defaultFor(MyCreatorId.class)).in((builder, creatorId) -> builder.setCreatorId(creatorId.value()))
// ... 其他字段
.use(Arbitraries.strings().alpha().ofLength(10, 20)).in(MyComplexClass.Builder::setPayload)
.build(MyComplexClass.Builder::build);
}
}在测试中使用:
class MyPropertiesWithTypes {
@Property(tries = 5)
@Domain(MyDomainWithTypes.class)
public void testMyComplexClassGeneration(@ForAll MyComplexClass complexClass) {
System.out.println("Generated Complex以上就是jqwik中Arbitrary组合与复用策略深度解析的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号