首页 > Java > java教程 > 正文

jqwik中@Provide方法结合@ForAll处理集合类型参数的最佳实践

碧海醫心
发布: 2025-10-10 14:45:25
原创
746人浏览过

jqwik中@Provide方法结合@ForAll处理集合类型参数的最佳实践

本文探讨了在jqwik中使用@forall注解与@provide方法处理集合类型参数时常见的陷阱。核心内容包括:明确@domain注解的正确作用域(应应用于属性方法或测试类,而非@provide方法本身),以及当@provide方法需要生成集合类型的arbitrary时,应避免在参数中使用@forall,转而直接在方法体内构建集合arbitrary,以避免潜在的arbitrary查找失败和不必要的扁平化映射。通过遵循这些指导原则,可以有效避免cannotfindarbitraryexception,并编写出更健壮、意图更明确的基于属性的测试。

在jqwik中进行基于属性的测试时,我们经常需要自定义复杂类型的任意值生成器(Arbitrary)。@Provide方法是实现这一目标的关键,它允许我们定义如何生成特定类型的任意值。然而,当尝试在@Provide方法中使用@ForAll注解来接收集合类型参数时,开发者可能会遇到CannotFindArbitraryException,这通常是由于对@Domain注解的作用域和@Provide方法的预期用途存在误解。

问题剖析:@ForAll与@Provide集合参数的误用

考虑以下场景,我们有一个Name领域模型,并希望生成一个Set<String>,其中包含解析后的Name对象。一个常见的错误尝试是在@Provide方法中这样定义:

// 领域模型
public class Name {
  public final String first;
  public final String last;
  public Name(String f, String l) { 
    this.first = f;
    this.last = l;
  }
}

// jqwik领域上下文
public class NameDomain extends DomainContextBase {
  @Provide
  public Arbitrary<Name> arbName() {
    return Combinators.combine(
      Arbitraries.strings().alpha(), 
      Arbitraries.strings().alpha()
    ).as(Name::new);
  }
}

// 属性测试类中的错误尝试
public class NameProperties {
  @Provide
  @Domain(NameDomain.class) // 错误:@Domain不应在此处
  public Arbitrary<Set<String>> namesToParse(
    @ForAll @Size(min = 1, max = 4) Set<Name> names) {
    // 假设此处将Set<Name>转换为Set<String>
    // ... code here
    return Arbitraries.just(new HashSet<>()); // 示例返回
  }

  @Property
  public void namesAreParsed(@ForAll("namesToParse") Set<String> names) {
    // ... code here
  }
}
登录后复制

当运行上述代码时,jqwik会抛出CannotFindArbitraryException,指出无法为namesToParse方法中Set<Name>类型的参数找到Arbitrary。这背后的原因是@Domain注解的错误放置,以及对@Provide方法中@ForAll参数处理机制的误解。

解决方案一:@Domain注解的正确作用域

@Domain注解的目的是将一个DomainContext关联到属性方法或整个测试类,从而使该上下文中的@Provide方法生成的Arbitrary对这些属性方法可见。它不应直接应用于@Provide方法本身。

正确的@Domain注解放置方式有两种:

  1. 应用于属性方法:

    public class NameProperties {
      // ... 其他代码 ...
    
      @Property
      @Domain(NameDomain.class) // 正确:应用于属性方法
      public void namesAreParsed(@ForAll("namesToParse") Set<String> names) {
        // ... code here
      }
    }
    登录后复制
  2. 应用于整个测试类:

    @Domain(NameDomain.class) // 正确:应用于测试类
    class NameProperties { 
      // ... 其他代码 ...
    
      @Property
      public void namesAreParsed(@ForAll("namesToParse") Set<String> names) {
        // ... code here
      }
    }
    登录后复制

通过将@Domain注解放置在正确的位置,NameDomain中提供的Arbitrary<Name>就能被NameProperties类中的属性方法识别和使用。然而,即使@Domain放置正确,原始的@Provide方法定义仍然存在问题。

怪兽AI数字人
怪兽AI数字人

数字人短视频创作,数字人直播,实时驱动数字人

怪兽AI数字人 44
查看详情 怪兽AI数字人

解决方案二:重构@Provide方法以直接构建集合Arbitrary

在@Provide方法中,当您需要生成一个复杂类型(如集合)的Arbitrary时,通常不建议在其参数中使用@ForAll。这是因为在@Provide方法中使用@ForAll参数,jqwik会采用扁平化映射(flat mapping)的方式来处理这些参数,这使得逻辑变得复杂,并且可能不是您期望的行为。

对于生成集合类型的Arbitrary,最清晰和推荐的方法是在@Provide方法体内直接构建所需的Arbitrary。您可以使用Arbitraries.defaultFor(Type.class)来获取指定类型的默认Arbitrary,然后利用其链式调用方法来构建集合。

以下是重构后的namesToParse方法示例:

import net.jqwik.api.*;
import net.jqwik.api.arbitraries.SetArbitrary;
import net.jqwik.api.domains.DomainContextBase;
import net.jqwik.api.domains.Domain;

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

// 领域模型
public class Name {
  public final String first;
  public final String last;
  public Name(String f, String l) { 
    this.first = f;
    this.last = l;
  }
}

// jqwik领域上下文
public class NameDomain extends DomainContextBase {
  @Provide
  public Arbitrary<Name> arbName() {
    return Combinators.combine(
      Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(10), // 增加长度限制
      Arbitraries.strings().alpha().ofMinLength(1).ofMaxLength(10)
    ).as(Name::new);
  }
}

// 属性测试
@Domain(NameDomain.class) // @Domain应用于测试类
public class NameProperties {

  @Provide
  public Arbitrary<Set<String>> namesToParse() {
    // 直接在方法体内构建Set<Name>的Arbitrary
    SetArbitrary<Name> namesArbitrary = Arbitraries.defaultFor(Name.class)
                                                  .set().ofMinSize(1).ofMaxSize(4);

    // 将Set<Name>映射为Set<String>
    return namesArbitrary.map(nameSet -> 
        nameSet.stream()
               .map(n -> n.first + " " + n.last) // 示例:将Name对象转换为字符串
               .collect(Collectors.toSet())
    );
  }

  @Property
  public void namesAreParsed(@ForAll("namesToParse") Set<String> names) {
    // 确保生成的集合不为空且大小在预期范围内
    System.out.println("Generated names: " + names);
    Assertions.assertThat(names).isNotEmpty();
    Assertions.assertThat(names.size()).isBetween(1, 4);
    // ... 实际的解析和验证逻辑
  }
}
登录后复制

在这个重构后的@Provide方法中:

  • 我们不再使用@ForAll参数。
  • 我们通过Arbitraries.defaultFor(Name.class)获取了Name类型的默认Arbitrary,该Arbitrary的生成逻辑由NameDomain中的arbName()方法提供。
  • 接着,我们使用.set().ofMinSize(1).ofMaxSize(4)链式调用来创建一个生成Set<Name>的Arbitrary,并指定了集合的大小约束。
  • 最后,使用.map()方法将生成的Set<Name>转换为Set<String>,这正是我们namesToParse方法期望返回的类型。

这种方式清晰地表达了namesToParse方法的目标:它提供了一个生成Set<String>的Arbitrary,并且这个Set<String>是基于Name对象生成的。

总结与最佳实践

在使用jqwik进行高级属性测试时,请牢记以下几点:

  1. @Domain注解的作用域: DomainContext通过@Domain注解关联到属性方法测试类,而不是@Provide方法本身。这确保了DomainContext中定义的Arbitrary对属性测试逻辑可见。
  2. @Provide方法的职责: Provide方法的核心职责是返回一个Arbitrary实例,该实例定义了如何生成特定类型的值。
  3. 避免@Provide方法中的@ForAll参数(特别是对于集合): 在@Provide方法中使用@ForAll参数会导致jqwik进行扁平化映射,这通常不是您在生成复杂类型或集合Arbitrary时所期望的行为。
  4. 直接构建Arbitrary: 当您需要一个复杂类型(如Set<T>、List<T>等)的Arbitrary时,最佳实践是在@Provide方法体内直接使用Arbitraries.defaultFor(Type.class)结合链式调用(如.set()、.list()、.map()、.filter()等)来构建所需的Arbitrary。这使得代码意图更明确,也更易于理解和维护。

遵循这些指导原则,您将能够更有效地利用jqwik的强大功能,编写出健壮且可读性强的基于属性的测试。

以上就是jqwik中@Provide方法结合@ForAll处理集合类型参数的最佳实践的详细内容,更多请关注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号