
本文深入探讨了在jqwik中如何高效地组合和复用`arbitrary`生成器,以构建复杂领域对象的测试数据。我们将介绍多种策略,包括静态方法、基于类型和注解的解析,以及跨领域共享生成器的方法,旨在提升属性测试代码的模块化、可读性和可维护性。
在属性测试框架jqwik中,Arbitrary是生成测试数据的核心组件。当测试的领域对象结构复杂,包含多个由基本类型Arbitrary派生而来的字段时,如何有效地组合这些生成器,并确保它们在不同的测试类或测试领域中可复用,是一个常见的挑战。本文将详细阐述jqwik提供的多种策略,帮助开发者构建清晰、可维护的测试数据生成逻辑。
在深入探讨复杂组合之前,有必要澄清jqwik中@ForAll注解的一些基本行为,这对于理解Arbitrary的复用至关重要。
@ForAll注解不仅限于在@Property标注的方法中使用,它同样可以在@Provide标注的方法以及领域(Domain)类中发挥作用。这使得我们可以在提供自定义Arbitrary时,利用其他已定义的Arbitrary来构造更复杂的生成器。
例如,在一个领域类中,我们可以定义一个生成字符串长度的Arbitrary,并将其注入到另一个生成字符串的Arbitrary中:
import net.jqwik.api.*;
import net.jqwik.api.domains.DomainContextBase;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class MyDomain extends DomainContextBase {
@Provide
public Arbitrary<String> strings(@ForAll("lengths") int length) {
return Arbitraries.strings().alpha().ofLength(length);
}
@Provide
public Arbitrary<Integer> lengths() {
return Arbitraries.integers().between(3, 10);
}
// 此方法定义的Arbitrary<Integer>不会被strings()方法使用,
// 因为其没有通过@ForAll("negatives")显式引用
@Provide
public Arbitrary<Integer> negatives() {
return Arbitraries.integers().between(-100, -10);
}
}
class MyProperties {
@Property(tries = 5)
@Domain(MyDomain.class)
void printOutAlphaStringsWithLength3to10(@ForAll String stringsFromDomain) {
System.out.println(stringsFromDomain);
assertThat(stringsFromDomain).hasSizeBetween(3, 10);
assertThat(stringsFromDomain).matches("[a-zA-Z]+");
}
}在上述示例中,strings()方法通过@ForAll("lengths")引用了lengths()方法提供的Arbitrary
@ForAll("name")中的字符串引用是局部解析的。它只会在当前类、父类以及包含类中查找匹配的@Provide方法。这种设计旨在避免复杂的全局字符串引用魔法,保持代码的清晰性和可预测性。
假设我们有一个复杂的领域对象MyComplexClass,它包含多个字符串字段,这些字符串可能具有特定的格式(如UUID、正整数ID等)。
public class MyComplexClass {
private final String id; // positive-integer shaped
private final String recordId; // uuid-shaped
private final String creatorId; // positive-integer shaped
private final String editorId; // positive-integer shaped
private final String nonce; // uuid-shaped
private final String payload; // random string
// 假设有构造函数或Builder模式
public 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;
}
public static MyComplexClassBuilder newBuilder() {
return new MyComplexClassBuilder();
}
// Getter方法...
public String getId() { return id; }
public String getRecordId() { return recordId; }
public String getCreatorId() { return creatorId; }
public String getEditorId() { return editorId; }
public String getNonce() { return nonce; }
public String getPayload() { return payload; }
public static class MyComplexClassBuilder {
private String id;
private String recordId;
private String creatorId;
private String editorId;
private String nonce;
private String payload;
public MyComplexClassBuilder setId(String id) { this.id = id; return this; }
public MyComplexClassBuilder setRecordId(String recordId) { this.recordId = recordId; return this; }
public MyComplexClassBuilder setCreatorId(String creatorId) { this.creatorId = creatorId; return this; }
public MyComplexClassBuilder setEditorId(String editorId) { this.editorId = editorId; return this; }
public MyComplexClassBuilder setNonce(String nonce) { this.nonce = nonce; return this; }
public MyComplexClassBuilder setPayload(String payload) { this.payload = payload; return this; }
public MyComplexClass build() {
return new MyComplexClass(id, recordId, creatorId, editorId, nonce, payload);
}
}
}以下是几种组合和复用这些基础Arbitrary
对于在单个领域或相关领域内共享生成器,直接创建静态Arbitrary方法并直接调用它们是一种“足够好”的简单方法。
import net.jqwik.api.*;
import java.util.UUID;
import java.util.Set;
public class SharedStringArbitraries {
public static Arbitrary<String> arbUuidString() {
return Combinators.combine(
Arbitraries.longs(), Arbitraries.longs(), Arbitraries.of(Set.of('8', '9', 'a', 'b')))
.as((l1, l2, y) -> {
StringBuilder b = new StringBuilder(new UUID(l1, l2).toString());
b.setCharAt(14, '4'); // UUID version 4
b.setCharAt(19, y); // UUID variant (8, 9, a, b)
return b.toString(); // 返回String,而不是UUID对象
});
}
public static Arbitrary<String> arbNumericIdString() {
return Arbitraries.integers().between(1, Integer.MAX_VALUE).map(i -> "" + i);
}
public static Arbitrary<String> arbRandomString() {
return Arbitraries.strings().alpha().ofMinLength(5).ofMaxLength(20);
}
}然后,可以在@Provide方法中直接调用这些静态方法:
import net.jqwik.api.*;
import net.jqwik.api.domains.DomainContextBase;
class MyDomain extends DomainContextBase {
@Provide
public Arbitrary<MyComplexClass> arbMyComplexClass() {
return Builders.withBuilder(MyComplexClass::newBuilder)
.use(SharedStringArbitraries.arbNumericIdString()).in(MyComplexClass.MyComplexClassBuilder::setId)
.use(SharedStringArbitraries.arbUuidString()).in(MyComplexClass.MyComplexClassBuilder::setRecordId)
.use(SharedStringArbitraries.arbNumericIdString()).in(MyComplexClass.MyComplexClassBuilder::setCreatorId)
.use(SharedStringArbitraries.arbNumericIdString()).in(MyComplexClass.MyComplexClassBuilder::setEditorId)
.use(SharedStringArbitraries.arbUuidString()).in(MyComplexClass.MyComplexClassBuilder::setNonce)
.use(SharedStringArbitraries.arbRandomString()).in(MyComplexClass.MyComplexClassBuilder::setPayload)
.build(MyComplexClass.MyComplexClassBuilder::build);
}
}
class MyComplexClassProperties {
@Property(tries = 5)
@Domain(MyDomain.class)
void checkMyComplexClass(@ForAll MyComplexClass instance) {
System.out.println("Generated MyComplexClass: " + instance.getId() + ", " + instance.getRecordId() + ", ...");
// 进行断言
}
}这种方法简单直接,但当需要跨不相关的领域共享生成器时,可能会导致代码重复或依赖管理不便。
为了更好地实现跨领域共享和类型安全,可以引入值类型(Value Type)来代替原始类型。例如,将普通的String字段替换为RecordId、UuidString等自定义类型。这样,jqwik就可以根据类型自动解析对应的Arbitrary。
// 定义值类型
record UuidString(String value) {}
record NumericIdString(String value) {}
record RandomPayloadString(String value) {}
// MyComplexClass现在使用值类型
public class MyComplexClassWithType {
private final NumericIdString id;
private final UuidString recordId;
private final NumericIdString creatorId;
private final NumericIdString editorId;
private final UuidString nonce;
private final RandomPayloadString payload;
public MyComplexClassWithType(NumericIdString id, UuidString recordId, NumericIdString creatorId, NumericIdString editorId, UuidString nonce, RandomPayloadString payload) {
this.id = id;
this.recordId = recordId;
this.creatorId = creatorId;
this.editorId = editorId;
this.nonce = nonce;
this.payload = payload;
}
public static MyComplexClassTypeBuilder newBuilder() {
return new MyComplexClassTypeBuilder();
}
// Getter方法...
public NumericIdString getId() { return id; }
public UuidString getRecordId() { return recordId; }
public NumericIdString getCreatorId() { return creatorId; }
public NumericIdString getEditorId() { return editorId; }
public UuidString getNonce() { return nonce; }
public RandomPayloadString getPayload() { return payload; }
public static class MyComplexClassTypeBuilder {
private NumericIdString id;
private UuidString recordId;
private NumericIdString creatorId;
private NumericIdString editorId;
private UuidString nonce;
private RandomPayloadString payload;
public MyComplexClassTypeBuilder setId(NumericIdString id) { this.id = id; return this; }
public MyComplexClassTypeBuilder setRecordId(UuidString recordId) { this.recordId = recordId; return this; }
public MyComplexClassTypeBuilder setCreatorId(NumericIdString creatorId) { this.creatorId = creatorId; return this; }
public MyComplexClassTypeBuilder setEditorId(NumericIdString editorId) { this.editorId = editorId; return this; }
public MyComplexClassTypeBuilder setNonce(UuidString nonce) { this.nonce = nonce; return this; }
public MyComplexClassTypeBuilder setPayload(RandomPayloadString payload) { this.payload = payload; return this; }
public MyComplexClassWithType build() {
return new MyComplexClassWithType(id, recordId, creatorId, editorId, nonce, payload);
}
}
}然后,在领域类中为这些值类型提供Arbitrary:
import net.jqwik.api.*;
import net.jqwik.api.domains.DomainContextBase;
import java.util.UUID;
import java.util.Set;
class MyTypeBasedDomain extends DomainContextBase {
@Provide
public Arbitrary<UuidString> arbUuidString() {
return Combinators.combine(
Arbitraries.longs(), Arbitraries.longs(), Arbitraries.of(Set.of('8', '9', 'a', 'b')))
.as((l1, l2, y) -> {
StringBuilder b = new StringBuilder(new UUID(l1, l2).toString());
b.setCharAt(14, '4');
b.setCharAt(19, y);
return new UuidString(b.toString());
});
}
@Provide
public Arbitrary<NumericIdString> arbNumericIdString() {
return Arbitraries.integers().between(1, Integer.MAX_VALUE).map(i -> new NumericIdString("" + i));
}
@Provide
public Arbitrary<RandomPayloadString> arbRandomPayloadString() {
return Arbitraries.strings().alpha().ofMinLength(5).ofMaxLength(20).map(RandomPayloadString::new);
}
@Provide
public Arbitrary<MyComplexClassWithType> arbMyComplexClassWithType() {
return Builders.withBuilder(MyComplexClassWithType::newBuilder)
.use(Arbitraries.defaultFor(NumericIdString.class)).in(MyComplexClassWithType.MyComplexClassTypeBuilder::setId)
.use(Arbitraries.defaultFor(UuidString.class)).in(MyComplexClassWithType.MyComplexClassTypeBuilder::setRecordId)
.use(Arbitraries.defaultFor(NumericIdString.class)).in(MyComplexClassWithType.MyComplexClassTypeBuilder::setCreatorId)
.use(Arbitraries.defaultFor(NumericIdString.class)).in(MyComplexClassWithType.MyComplexClassTypeBuilder::setEditorId)
.use(Arbitraries.defaultFor(UuidString.class)).in(MyComplexClassWithType.MyComplexClassTypeBuilder::setNonce)
.use(Arbitraries.defaultFor(RandomPayloadString.class)).in(MyComplexClassWithType.MyComplexClassTypeBuilder::setPayload)
.build(MyComplexClassWithType.MyComplexClassTypeBuilder::build);
}
}
class MyComplexClassTypeProperties {
@Property(tries = 5)
@Domain(MyTypeBasedDomain.class)
void checkMyComplexClassWithType(@ForAll MyComplexClassWithType instance) {
System.out.println("Generated MyComplexClassWithType: " + instance.getId().value() + ", " + instance.getRecordId().value() + ", ...");
// 进行断言
}
}这种方法提高了类型安全性,并允许jqwik通过Arbitraries.defaultFor(Type.class)自动发现并使用相应的Arbitrary。
有时,即使是相同的类型,也可能需要生成不同语义的值。例如,两个String字段,一个表示“名称”,另一个表示“十六进制数”。在这种情况下,可以使用自定义注解来区分这些变体。
首先,定义自定义注解:
import java.lang.annotation.*;
public class MyAnnotations {
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {}
@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HexNumber {}
}然后,在@Provide方法中,利用TypeUsage参数检查目标类型是否带有特定注解,从而返回不同的Arbitrary:
import net.jqwik.api.*;
import net.jqwik.api.domains.DomainContextBase;
import net.jqwik.api.providers.TypeUsage;
class MyAnnotationBasedDomain extends DomainContextBase {
// 假设存在一个提供基础整数的领域
@Domain(MyNumbersDomain.class) // 可以组合其他领域
public static class MyNumbersDomain extends DomainContextBase {
@Provide
Arbitrary<Integer> numbers() {
return Arbitraries.integers().between(0, 255);
}
}
@Provide
public Arbitrary<String> names(TypeUsage targetType) {
if (targetType.isAnnotated(MyAnnotations.Name.class)) {
return Arbitraries.strings().alpha().ofLength(5);
}
return null; // 如果不匹配,返回null,让jqwik尝试其他提供者
}
@Provide
public Arbitrary<String> numbers(TypeUsage targetType) {
if (targetType.isAnnotated(MyAnnotations.HexNumber.class)) {
// 使用MyNumbersDomain提供的Integer Arbitrary
return Arbitraries.defaultFor(Integer.class).map(Integer::toHexString);
}
return null;
}
}
class MyAnnotationProperties {
@Property(tries = 5)
@Domain(MyAnnotationBasedDomain.class)
void generateNamesAndHexNumbers(
@ForAll @MyAnnotations.Name String aName,
@ForAll @MyAnnotations.HexNumber String aHexNumber
) {
System.out.println("Name: " + aName);
System.out.println("Hex Number: " + aHexNumber);
assertThat(aName).hasSize(5).matches("[a-zA-Z]+");
assertThat(aHexNumber).matches("[0-9a-fA-F]+");
}
}这种方法提供了极高的灵活性,允许在不改变基础类型的情况下,根据语义需求生成不同的数据。
jqwik允许通过在领域类上使用@Domain注解来组合多个领域。这意味着一个领域可以继承或引用另一个领域中定义的Arbitrary。
在上述“基于注解的类型变体区分”的例子中,MyAnnotationBasedDomain通过在其内部类MyNumbersDomain上使用@Domain(MyNumbersDomain.class)来引入MyNumbersDomain中定义的Arbitrary
这种机制使得我们可以将不同职责的Arbitrary组织到不同的领域类中,并在需要时进行组合,从而实现高度模块化的生成器管理。
无论采用哪种共享策略,最终目标通常是组合这些基础Arbitrary来构建复杂的领域对象。jqwik提供了Combinators和Builders两种强大的工具。
Combinators.combine(): 适用于当复杂对象可以通过一个构造函数或工厂方法直接从多个值构建时。它接受多个Arbitrary作为输入,并通过一个映射函数将它们组合成一个新的Arbitrary。
// 假设MyComplexClass有一个全参构造函数
@Provide
public Arbitrary<MyComplexClass> arbMyComplexClassWithCombinators() {
return Combinators.combine(
SharedStringArbitraries.arbNumericIdString(),
SharedStringArbitraries.arbUuidString(),
SharedStringArbitraries.arbNumericIdString(),
SharedStringArbitraries.arbNumericId以上就是jqwik中Arbitrary生成器的高效组合与复用策略的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号