首页 > Java > java教程 > 正文

jqwik中Arbitrary组合与复用策略深度解析

花韻仙語
发布: 2025-10-20 10:12:32
原创
398人浏览过

jqwik中Arbitrary组合与复用策略深度解析

本文深入探讨了jqwik中`arbitrary`的组合与复用策略,旨在帮助开发者高效生成复杂测试数据。文章首先纠正了`@forall`注解在`@provide`方法和领域中的使用误区,随后详细介绍了在不同场景下共享`arbitrary`的几种方法,包括静态函数、基于类型解析的自定义值类型,以及通过自定义注解实现更精细化的数据生成。通过丰富的示例代码,本文为构建可维护、可扩展的属性测试提供了实践指导。

1. jqwik中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<String>:

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);
    }
}
登录后复制

2. 理解@ForAll与@Provide的协同作用

在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());
    }
}
登录后复制

3. Arbitrary的共享策略

当我们需要在不同的测试类或领域中复用Arbitrary定义时,有几种不同的策略可以选择。

3.1 简单共享:静态Arbitrary函数

在单个领域内,或者当多个相关领域继承自同一个父类时,将Arbitrary定义为静态方法并直接调用是一种简单有效的共享方式。

优点:

  • 实现简单,易于理解。
  • 对于局部或紧密相关的Arbitrary共享非常方便。

缺点:

百度文心百中
百度文心百中

百度大模型语义搜索体验中心

百度文心百中 22
查看详情 百度文心百中
  • 当需要跨越不相关的领域共享时,可能导致类型不明确,或者需要手动传递大量参数。
  • 所有共享的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);
    }
}
登录后复制

3.2 基于类型解析的共享:引入值类型

当需要在不相关的领域之间共享Arbitrary,并且这些Arbitrary生成的数据具有特定的业务含义时,引入自定义值类型(Value Type)是一种推荐的实践。通过为特定类型的字符串或整数创建包装类,jqwik可以根据类型自动解析并提供相应的Arbitrary。

优点:

  • 类型安全:明确了数据的业务含义,避免了原始类型(如String)的滥用。
  • 自动解析:jqwik能够基于类型自动查找并使用匹配的ArbitraryProvider或@Provide方法。
  • 可读性强:代码意图更清晰。

缺点:

  • 可能导致创建大量的简单包装类,增加代码量。
  • 如果原始类(如MyComplexClass)无法修改以接受这些值类型,则需要在Arbitrary组合时进行类型转换。

示例:

  1. 定义值类型:

    // 定义表示特定业务含义的字符串类型
    public record MyId(String value) {}
    public record MyRecordId(String value) {}
    public record MyCreatorId(String value) {}
    // ... 其他需要区分的类型
    登录后复制
  2. 在领域中提供这些值类型对应的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);
        }
    }
    登录后复制
  3. 在测试中使用:

    class MyPropertiesWithTypes {
        @Property(tries = 5)
        @Domain(MyDomainWithTypes.class)
        public void testMyComplexClassGeneration(@ForAll MyComplexClass complexClass) {
            System.out.println("Generated Complex
    登录后复制

以上就是jqwik中Arbitrary组合与复用策略深度解析的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

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