登录
首页 >  文章 >  java教程

Jqwik生成器组合与复用技巧解析

时间:2025-11-05 09:24:29 303浏览 收藏

目前golang学习网上已经有很多关于文章的文章了,自己在初次阅读这些文章中,也见识到了很多学习思路;那么本文《Jqwik组合与复用Arbitrary生成器技巧》,也希望能帮助到大家,如果阅读完后真的对你学习文章有帮助,欢迎动动手指,评论留言并分享~

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

本文深入探讨了在jqwik中如何有效地组合和复用Arbitrary生成器,以构建复杂领域对象的测试数据。文章详细介绍了通过静态方法、类型化封装、以及自定义注解等多种策略,实现Arbitrary的共享与精细控制。同时,还涵盖了在`@Provide`方法和领域上下文中使用`@ForAll`的技巧,并提供了具体的代码示例,帮助开发者编写更灵活、可维护的属性测试。

jqwik中Arbitrary生成器的组合与复用

在jqwik进行属性测试时,我们经常需要为复杂的领域对象生成测试数据。这些复杂对象通常由多个基本类型或自定义类型组成,而为每个属性单独定义生成器既冗余又难以维护。本文将详细介绍如何在jqwik中有效地组合和复用Arbitrary生成器,从而提高测试代码的模块化和可读性。

理解@ForAll在@Provide和@Domain中的应用

在深入探讨组合策略之前,首先需要澄清一个常见的误解:@ForAll注解不仅限于@Property方法,它同样可以在@Provide方法和DomainContextBase类中有效使用。这为构建依赖于其他Arbitrary的复杂生成器提供了极大的灵活性。

考虑以下示例,一个MyDomain类定义了如何生成指定长度的字符串:

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);
    }
}

@Domain(MyDomain.class)
class MyProperties {
    @Property(tries = 10)
    void printOutAlphaStringsWithLength3to10(@ForAll String stringsFromDomain) {
        System.out.println(stringsFromDomain);
        assertThat(stringsFromDomain).hasSizeBetween(3, 10);
    }
}

注意事项:@ForAll("name")中的字符串引用(如"lengths")仅在本地(当前类、父类和包含类)进行解析。这种设计旨在避免过度依赖字符串的“魔法”引用,因为Java注解本身不支持方法引用。

组合复杂领域对象的Arbitrary

假设我们有一个复杂的领域对象MyComplexClass,它包含多种类型的字符串ID:

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;   // 随机字符串

    // 假设有建造者模式或全参数构造函数
    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;
    }

    // 假设有newBuilder()方法
    public static MyComplexClassBuilder newBuilder() {
        return new MyComplexClassBuilder();
    }

    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);
        }
    }

    // Getter methods
    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; }

    @Override
    public String toString() {
        return "MyComplexClass{" +
               "id='" + id + '\'' +
               ", recordId='" + recordId + '\'' +
               ", creatorId='" + creatorId + '\'' +
               ", editorId='" + editorId + '\'' +
               ", nonce='" + nonce + '\'' +
               ", payload='" + payload + '\'' +
               '}';
    }
}

为了生成MyComplexClass的实例,我们需要组合多个Arbitrary。以下是几种策略。

1. 使用静态Arbitrary函数直接调用

这是最直接且“足够好”的方案,适用于在单个领域或相关联的领域内共享生成器。你可以定义静态方法来提供特定的Arbitrary实例。

import net.jqwik.api.*;
import net.jqwik.api.builders.Builders;
import net.jqwik.api.domains.DomainContextBase;

import java.util.Set;
import java.util.UUID;

public class MyArbitraries {
    public static Arbitrary<String> arbUuidString() {
        // 简化UUID生成逻辑,实际可能更复杂
        return Arbitraries.strings().numeric().ofLength(32).map(s -> UUID.randomUUID().toString());
    }

    public static Arbitrary<String> arbNumericIdString() {
        return Arbitraries.integers().between(1, 10000).map(String::valueOf);
    }
}

class MyDomain extends DomainContextBase {
    @Provider
    public Arbitrary<MyComplexClass> arbMyComplexClass() {
        return Builders.withBuilder(MyComplexClass::newBuilder)
                .use(MyArbitraries.arbNumericIdString()).in(MyComplexClass.MyComplexClassBuilder::setId)
                .use(MyArbitraries.arbUuidString()).in(MyComplexClass.MyComplexClassBuilder::setRecordId)
                .use(MyArbitraries.arbNumericIdString()).in(MyComplexClass.MyComplexClassBuilder::setCreatorId)
                .use(MyArbitraries.arbNumericIdString()).in(MyComplexClass.MyComplexClassBuilder::setEditorId)
                .use(MyArbitraries.arbUuidString()).in(MyComplexClass.MyComplexClassBuilder::setNonce)
                .use(Arbitraries.strings().alpha().ofLength(10, 20)).in(MyComplexClass.MyComplexClassBuilder::setPayload)
                .build(MyComplexClass.MyComplexClassBuilder::build);
    }
}

这种方法简单明了,但当需要跨不相关的领域共享生成器,或者同一种基本类型(如String)需要有多种不同的生成逻辑时,可能会显得不足。

2. 基于类型解析的Arbitrary共享

当需要跨不相关的领域共享生成器时,引入值类型(Value Type)是一个非常有效的策略。通过为特定的数据形式创建封装类,jqwik可以根据类型自动解析对应的Arbitrary。

// 值类型示例
public record RecordId(String value) {
    public static Arbitrary<RecordId> arbitrary() {
        return Arbitrstrings().numeric().ofLength(32).map(s -> new RecordId(UUID.randomUUID().toString()));
    }
}

public record CreatorId(String value) {
    public static Arbitrary<CreatorId> arbitrary() {
        return Arbitraries.integers().between(1, 10000).map(i -> new CreatorId(String.valueOf(i)));
    }
}

// MyComplexClass更新为使用值类型
public class MyComplexClassWithType {
    private final CreatorId id;
    private final RecordId recordId;
    private final CreatorId creatorId;
    private final CreatorId editorId;
    private final RecordId nonce;
    private final String payload;

    public MyComplexClassWithType(CreatorId id, RecordId recordId, CreatorId creatorId, CreatorId editorId, RecordId nonce, String payload) {
        this.id = id;
        this.recordId = recordId;
        this.creatorId = creatorId;
        this.editorId = editorId;
        this.nonce = nonce;
        this.payload = payload;
    }

    public static MyComplexClassWithTypeBuilder newBuilder() {
        return new MyComplexClassWithTypeBuilder();
    }

    public static class MyComplexClassWithTypeBuilder {
        private CreatorId id;
        private RecordId recordId;
        private CreatorId creatorId;
        private CreatorId editorId;
        private RecordId nonce;
        private String payload;

        public MyComplexClassWithTypeBuilder setId(CreatorId id) { this.id = id; return this; }
        public MyComplexClassWithTypeBuilder setRecordId(RecordId recordId) { this.recordId = recordId; return this; }
        public MyComplexClassWithTypeBuilder setCreatorId(CreatorId creatorId) { this.creatorId = creatorId; return this; }
        public MyComplexClassWithTypeBuilder setEditorId(CreatorId editorId) { this.editorId = editorId; return this; }
        public MyComplexClassWithTypeBuilder setNonce(RecordId nonce) { this.nonce = nonce; return this; }
        public MyComplexClassWithTypeBuilder setPayload(String payload) { this.payload = payload; return this; }

        public MyComplexClassWithType build() {
            return new MyComplexClassWithType(id, recordId, creatorId, editorId, nonce, payload);
        }
    }
}

// 在DomainContextBase中提供Arbitrary
class MyTypedDomain extends DomainContextBase {
    @Provide
    public Arbitrary<RecordId> recordIdArbitrary() {
        return RecordId.arbitrary();
    }

    @Provide
    public Arbitrary<CreatorId> creatorIdArbitrary() {
        return CreatorId.arbitrary();
    }

    @Provider
    public Arbitrary<MyComplexClassWithType> arbMyComplexClassWithType() {
        return Builders.withBuilder(MyComplexClassWithType::newBuilder)
                // jqwik会根据类型自动查找对应的@Provide方法
                .use(Arbitraries.defaultFor(CreatorId.class)).in(MyComplexClassWithType.MyComplexClassWithTypeBuilder::setId)
                .use(Arbitraries.defaultFor(RecordId.class)).in(MyComplexClassWithType.MyComplexClassWithTypeBuilder::setRecordId)
                .use(Arbitraries.defaultFor(CreatorId.class)).in(MyComplexClassWithType.MyComplexClassWithTypeBuilder::setCreatorId)
                .use(Arbitraries.defaultFor(CreatorId.class)).in(MyComplexClassWithType.MyComplexClassWithTypeBuilder::setEditorId)
                .use(Arbitraries.defaultFor(RecordId.class)).in(MyComplexClassWithType.MyComplexClassWithTypeBuilder::setNonce)
                .use(Arbitraries.strings().alpha().ofLength(10, 20)).in(MyComplexClassWithType.MyComplexClassWithTypeBuilder::setPayload)
                .build(MyComplexClassWithType.MyComplexClassWithTypeBuilder::build);
    }
}

这种方法通过引入强类型,使得代码更加清晰,并利用了jqwik的类型解析能力,是推荐的模式。

3. 基于注解的Arbitrary解析

当同一种基本类型(如String或Integer)需要根据上下文生成不同的值时,可以使用自定义注解来标记参数,并在@Provide方法中通过TypeUsage对象检查这些注解。

import net.jqwik.api.*;
import net.jqwik.api.domains.DomainContextBase;
import net.jqwik.api.providers.TypeUsage;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 假设我们有一个基础的数字生成器领域
class MyNumbers extends DomainContextBase {
    @Provide
    Arbitrary<Integer> numbers() {
        return Arbitraries.integers().between(0, 255);
    }
}

@Domain(MyNumbers.class) // MyDomain可以使用MyNumbers中定义的Arbitrary
class MyAnnotatedDomain extends DomainContextBase {

    @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @interface Name {} // 用于标记生成名称的字符串

    @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @interface HexNumber {} // 用于标记生成十六进制数字的字符串

    @Provide
    public Arbitrary<String> names(TypeUsage targetType) {
        if (targetType.isAnnotated(Name.class)) {
            return Arbitraries.strings().alpha().ofLength(5);
        }
        return null; // 如果没有匹配的注解,返回null表示此Provider不适用
    }

    @Provide
    public Arbitrary<String> numbers(TypeUsage targetType) {
        if (targetType.isAnnotated(HexNumber.class)) {
            // 可以直接使用MyNumbers中定义的Integer Arbitrary
            return Arbitraries.defaultFor(Integer.class).map(Integer::toHexString);
        }
        return null;
    }
}

// 使用自定义注解进行属性测试
class MyAnnotatedProperties {
    @Property(tries = 10)
    @Domain(MyAnnotatedDomain.class)
    void generateNamesAndHexNumbers(
            @ForAll @MyAnnotatedDomain.Name String aName,
            @ForAll @MyAnnotatedDomain.HexNumber String aHexNumber
    ) {
        System.out.println("Name: " + aName + ", Hex: " + aHexNumber);
        assertThat(aName).matches("[a-zA-Z]{5}");
        assertThat(aHexNumber).matches("[0-9a-fA-F]+");
    }
}

这种方法提供了极高的灵活性,允许为同一类型定义多个不同的生成策略,并通过注解在需要的地方精确地引用它们。

总结与建议

选择哪种Arbitrary组合策略取决于你的具体需求和项目结构:

  • 静态Arbitrary函数: 适用于简单场景,或在单个测试类、紧密相关的领域中共享生成器。它直接且易于理解。
  • 类型化封装(值类型): 强烈推荐用于复杂领域对象。它提升了代码的清晰度和类型安全性,并充分利用了jqwik的类型解析能力,使得生成器更容易被发现和复用。
  • 注解驱动的Arbitrary: 适用于需要为相同基础类型(如String)提供多种不同生成逻辑的复杂场景。它提供了最细粒度的控制,但引入了额外的注解定义,增加了少量复杂性。

在实践中,通常会结合使用这些策略。例如,你可以使用类型化封装来定义核心领域对象的Arbitrary,并结合注解来处理那些需要特殊格式但又不值得创建独立值类型的字符串属性。通过合理组织和设计你的Arbitrary生成器,可以显著提高jqwik属性测试的效率和可维护性。

以上就是《Jqwik生成器组合与复用技巧解析》的详细内容,更多关于的资料请关注golang学习网公众号!

相关阅读
更多>
最新阅读
更多>
课程推荐
更多>