0

0

jqwik中Arbitrary生成器的高效组合与复用策略

霞舞

霞舞

发布时间:2025-10-20 10:26:08

|

578人浏览过

|

来源于php中文网

原创

jqwik中Arbitrary生成器的高效组合与复用策略

本文深入探讨了在jqwik中如何高效地组合和复用`arbitrary`生成器,以构建复杂领域对象的测试数据。我们将介绍多种策略,包括静态方法、基于类型和注解的解析,以及跨领域共享生成器的方法,旨在提升属性测试代码的模块化、可读性和可维护性。

引言

在属性测试框架jqwik中,Arbitrary是生成测试数据的核心组件。当测试的领域对象结构复杂,包含多个由基本类型Arbitrary派生而来的字段时,如何有效地组合这些生成器,并确保它们在不同的测试类或测试领域中可复用,是一个常见的挑战。本文将详细阐述jqwik提供的多种策略,帮助开发者构建清晰、可维护的测试数据生成逻辑。

jqwik中Arbitrary的基本复用机制

在深入探讨复杂组合之前,有必要澄清jqwik中@ForAll注解的一些基本行为,这对于理解Arbitrary的复用至关重要。

@ForAll注解的灵活运用

@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 strings(@ForAll("lengths") int length) {
        return Arbitraries.strings().alpha().ofLength(length);
    }

    @Provide
    public Arbitrary lengths() {
        return Arbitraries.integers().between(3, 10);
    }

    // 此方法定义的Arbitrary不会被strings()方法使用,
    // 因为其没有通过@ForAll("negatives")显式引用
    @Provide
    public Arbitrary 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,从而生成长度在3到10之间的字母字符串。

字符串引用的作用域

@ForAll("name")中的字符串引用是局部解析的。它只会在当前类、父类以及包含类中查找匹配的@Provide方法。这种设计旨在避免复杂的全局字符串引用魔法,保持代码的清晰性和可预测性。

组合复杂对象Arbitrary的挑战与策略

假设我们有一个复杂的领域对象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以生成MyComplexClass的策略。

1. 直接调用静态Arbitrary方法

对于在单个领域或相关领域内共享生成器,直接创建静态Arbitrary方法并直接调用它们是一种“足够好”的简单方法。

import net.jqwik.api.*;
import java.util.UUID;
import java.util.Set;

public class SharedStringArbitraries {
    public static Arbitrary 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 arbNumericIdString() {
        return Arbitraries.integers().between(1, Integer.MAX_VALUE).map(i -> "" + i);
    }

    public static Arbitrary 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 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() + ", ...");
        // 进行断言
    }
}

这种方法简单直接,但当需要跨不相关的领域共享生成器时,可能会导致代码重复或依赖管理不便。

2. 基于类型的解析与值类型

为了更好地实现跨领域共享和类型安全,可以引入值类型(Value Type)来代替原始类型。例如,将普通的String字段替换为RecordId、UuidString等自定义类型。这样,jqwik就可以根据类型自动解析对应的Arbitrary。

有道智云AI开放平台
有道智云AI开放平台

有道智云AI开放平台

下载
// 定义值类型
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 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 arbNumericIdString() {
        return Arbitraries.integers().between(1, Integer.MAX_VALUE).map(i -> new NumericIdString("" + i));
    }

    @Provide
    public Arbitrary arbRandomPayloadString() {
        return Arbitraries.strings().alpha().ofMinLength(5).ofMaxLength(20).map(RandomPayloadString::new);
    }

    @Provide
    public Arbitrary 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。

3. 基于注解的类型变体区分

有时,即使是相同的类型,也可能需要生成不同语义的值。例如,两个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 numbers() {
            return Arbitraries.integers().between(0, 255);
        }
    }

    @Provide
    public Arbitrary names(TypeUsage targetType) {
        if (targetType.isAnnotated(MyAnnotations.Name.class)) {
            return Arbitraries.strings().alpha().ofLength(5);
        }
        return null; // 如果不匹配,返回null,让jqwik尝试其他提供者
    }

    @Provide
    public Arbitrary 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]+");
    }
}

这种方法提供了极高的灵活性,允许在不改变基础类型的情况下,根据语义需求生成不同的数据。

跨领域Arbitrary的共享

jqwik允许通过在领域类上使用@Domain注解来组合多个领域。这意味着一个领域可以继承或引用另一个领域中定义的Arbitrary。

在上述“基于注解的类型变体区分”的例子中,MyAnnotationBasedDomain通过在其内部类MyNumbersDomain上使用@Domain(MyNumbersDomain.class)来引入MyNumbersDomain中定义的Arbitrary。然后,在numbers()方法中,可以通过Arbitraries.defaultFor(Integer.class)来获取并使用这个Arbitrary。

这种机制使得我们可以将不同职责的Arbitrary组织到不同的领域类中,并在需要时进行组合,从而实现高度模块化的生成器管理。

使用Combinators和Builders组合复杂对象

无论采用哪种共享策略,最终目标通常是组合这些基础Arbitrary来构建复杂的领域对象。jqwik提供了Combinators和Builders两种强大的工具

  • Combinators.combine(): 适用于当复杂对象可以通过一个构造函数或工厂方法直接从多个值构建时。它接受多个Arbitrary作为输入,并通过一个映射函数将它们组合成一个新的Arbitrary。

    // 假设MyComplexClass有一个全参构造函数
    @Provide
    public Arbitrary arbMyComplexClassWithCombinators() {
        return Combinators.combine(
                SharedStringArbitraries.arbNumericIdString(),
                SharedStringArbitraries.arbUuidString(),
                SharedStringArbitraries.arbNumericIdString(),
                SharedStringArbitraries.arbNumericId

相关专题

更多
string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

312

2023.08.02

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

248

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

205

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1435

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

609

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

547

2024.03.22

php中定义字符串的方式
php中定义字符串的方式

php中定义字符串的方式:单引号;双引号;heredoc语法等等。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

539

2024.04.29

go语言字符串相关教程
go语言字符串相关教程

本专题整合了go语言字符串相关教程,阅读专题下面的文章了解更多详细内容。

158

2025.07.29

php源码安装教程大全
php源码安装教程大全

本专题整合了php源码安装教程,阅读专题下面的文章了解更多详细内容。

7

2025.12.31

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 2.1万人学习

C# 教程
C# 教程

共94课时 | 5.7万人学习

Java 教程
Java 教程

共578课时 | 40.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号