首页 > Java > java教程 > 正文

Jqwik中结合@ForAll、@Provide和集合类型正确定义任意生成器

心靈之曲
发布: 2025-10-10 15:05:00
原创
811人浏览过

Jqwik中结合@ForAll、@Provide和集合类型正确定义任意生成器

本文旨在解决jqwik测试框架中,当尝试在`@provide`方法中使用`@forall`注解与集合类型时常遇到的`cannotfindarbitraryexception`。我们将深入探讨`@domain`注解的正确作用域,并展示如何优雅地构建基于集合的任意生成器,避免不必要的扁平化映射,从而实现更灵活和高效的属性测试。

引言:@Provide方法中@ForAll与集合类型的挑战

在Jqwik属性测试框架中,我们经常需要为复杂类型或集合类型定义自定义的任意生成器(Arbitrary)。@Provide注解是实现这一目标的关键机制。然而,当尝试在一个@Provide方法内部,通过@ForAll注解引入一个集合类型的参数,并期望该集合的元素来自特定的领域上下文(@Domain)时,开发者可能会遇到net.jqwik.api.CannotFindArbitraryException。

考虑以下一个典型的场景,我们有一个Name领域模型,并为其定义了一个任意生成器arbName()。现在,我们希望提供一个Arbitrary<Set<String>>,其中Set<String>是从Set<Name>转换而来。最初,开发者可能会尝试如下结构:

// domain model
public class Name {
  public final String first;
  public final String last;
  public Name(String f, String l) {
    this.first = f;
    this.last = l;
  }
}

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

// properties test (Initial Attempt)
public class NameProperties {
  @Provide
  @Domain(NameDomain.class) // Problematic placement
  public Arbitrary<Set<String>> namesToParse(
    @ForAll @Size(min = 1, max = 4) Set<Name> names) {
    // ... code here to convert Set<Name> to Set<String>
    // This method is intended to provide Arbitrary<Set<String>>
    return Arbitrjust.just(names.stream()
                                .map(n -> n.first + " " + n.last)
                                .collect(Collectors.toSet()));
  }

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

运行上述代码,Jqwik会抛出CannotFindArbitraryException,指出无法为namesToParse方法中的Set<Name>参数找到任意生成器。这通常是由于对@Domain注解作用域和@Provide方法中@ForAll参数行为的误解造成的。

理解@Domain注解的作用域

解决上述问题的首要步骤是正确理解@Domain注解的作用域。@Domain注解用于指定属性测试方法(即被@Property注解的方法)或其包含的测试类(Test Class)的领域上下文。它告诉Jqwik在当前作用域内,当遇到需要生成特定类型的任意值时,应该从哪个DomainContext中查找相应的@Provide方法。

关键点在于:@Domain不应直接应用于一个自身带有@ForAll参数的@Provide方法。 @Provide方法本身是用于定义和提供Arbitrary实例的,而不是直接消费领域上下文中的任意值。当一个@Provide方法需要依赖其他Arbitrary来构建自己的Arbitrary时,它应该通过Jqwik的Arbitraries工具类或组合器来显式地获取和操作这些Arbitrary,而不是通过其方法参数上的@ForAll注解。

因此,@Domain注解的正确放置方式有以下两种:

  1. 应用于整个测试类: 如果测试类中的多个属性测试方法都依赖于同一个领域上下文,将@Domain注解放在测试类声明上是最简洁的方式。
  2. 应用于单个属性测试方法: 如果只有特定的属性测试方法需要某个领域上下文,则可以将其直接放在该@Property方法上。

根据我们的示例,将@Domain(NameDomain.class)从namesToParse方法上移除,并将其放置在NameProperties类上,是解决CannotFindArbitraryException的第一步。

FashionLabs
FashionLabs

AI服装模特、商品图,可商用,低价提升销量神器

FashionLabs 38
查看详情 FashionLabs
// properties test (Corrected @Domain placement)
@Domain(NameDomain.class) // Correct: Applied to the test class
public class NameProperties {
  @Provide
  // @Domain(NameDomain.class) // Incorrect here
  public Arbitrary<Set<String>> namesToParse(
    @ForAll @Size(min = 1, max = 4) Set<Name> names) {
    // ... still problematic, but @Domain is now correctly placed
    return Arbitrjust.just(names.stream()
                                .map(n -> n.first + " " + n.last)
                                .collect(Collectors.toSet()));
  }

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

优化@Provide方法:构建集合类型任意生成器的正确姿势

即使@Domain注解放置正确,原始namesToParse方法的设计仍然不符合Jqwik的最佳实践。当@Provide方法带有@ForAll参数时,Jqwik会将其视为一个“扁平化映射”(flat mapping)操作,这通常不是我们希望构建一个集合类型Arbitrary时的行为。我们通常希望的是直接在@Provide方法内部组合或转换Arbitrary,而不是依赖于方法参数的注入。

推荐的解决方案是在@Provide方法内部直接构建目标集合类型的Arbitrary,利用Jqwik强大的Arbitraries工具类和链式调用。 这样可以更清晰、更灵活地定义任意生成逻辑。

以下是优化namesToParse方法的步骤:

  1. 获取基础元素的任意生成器: Jqwik提供了Arbitraries.defaultFor(Type.class)方法,它会智能地查找为指定类型定义的任意生成器。由于我们将@Domain(NameDomain.class)放置在了NameProperties类上,Arbitraries.defaultFor(Name.class)将能够找到NameDomain中定义的Arbitrary<Name>。
  2. 转换为集合类型的任意生成器: 在获取到基础元素的Arbitrary<Name>之后,我们可以使用.set()、.list()等方法将其转换为集合类型的任意生成器。同时,可以通过ofMinSize()和ofMaxSize()等方法来限制集合的大小。
  3. 对集合进行映射或转换: 得到SetArbitrary<Name>之后,我们可以使用其map()方法对生成的Set<Name>进行整体转换,或者使用mapEach()方法对集合中的每个元素进行转换。

以下是优化后的namesToParse方法示例:

import net.jqwik.api.*;
import net.jqwik.api.arbitraries.SetArbitrary;
import net.jqwik.api.domains.DomainContextBase;
import java.util.Set;
import java.util.stream.Collectors;

// domain model (unchanged)
public class Name {
  public final String first;
  public final String last;
  public Name(String f, String l) {
    this.first = f;
    this.last = l;
  }
}

// jqwik domain context (unchanged)
public class NameDomain extends DomainContextBase {
  @Provide
  public Arbitrary<Name> arbName() {
    return Combinators.combine(
      Arbitraries.strings().alpha().ofMinLength(1), // Add min length for meaningful names
      Arbitraries.strings().alpha().ofMinLength(1)
    ).as(Name::new);
  }
}

// properties test (Fully optimized)
@Domain(NameDomain.class) // Correct @Domain placement for the test class
public class NameProperties {

  @Provide
  public Arbitrary<Set<String>> namesToParse() {
    // 1. 获取Name类型的任意生成器,Jqwik会从NameDomain中查找
    // 2. 将其转换为Set<Name>的任意生成器,并指定大小范围
    SetArbitrary<Name> namesArbitrary = Arbitraries.defaultFor(Name.class)
                                                    .set().ofMinSize(1).ofMaxSize(4);

    // 3. 对生成的Set<Name>进行映射,将其中的每个Name对象转换为String
    return namesArbitrary.map(nameSet -> nameSet.stream()
                                                .map(n -> n.first + " " + n.last)
                                                .collect(Collectors.toSet()));
  }

  @Property
  public void namesAreParsed(@ForAll("namesToParse") Set<String> names) {
    // 实际的解析逻辑和断言
    // System.out.println("Generated names: " + names);
    Assertions.assertNotNull(names);
    Assertions.assertFalse(names.isEmpty());
    Assertions.assertTrue(names.size() >= 1 && names.size() <= 4);
    // 假设我们期望每个字符串都是 "firstName lastName" 的格式
    for (String name : names) {
        Assertions.assertTrue(name.contains(" "));
        String[] parts = name.split(" ");
        Assertions.assertEquals(2, parts.length);
        Assertions.assertFalse(parts[0].isEmpty());
        Assertions.assertFalse(parts[1].isEmpty());
    }
  }
}
登录后复制

注意事项与总结

通过上述修正,我们解决了在Jqwik中为集合类型定义@Provide方法时遇到的常见问题。以下是关键的注意事项和最佳实践:

  • @Domain的作用域: 始终将@Domain注解应用于@Property方法或整个测试类,以指定领域上下文。不要将其应用于自身带有@ForAll参数的@Provide方法。
  • @Provide方法中的@ForAll参数: 除非你明确需要进行扁平化映射操作,否则应避免在@Provide方法的参数中使用@ForAll。这会导致行为上的混淆,并可能触发CannotFindArbitraryException。
  • 构建集合Arbitrary: 在@Provide方法内部,通过Arbitraries.defaultFor(Type.class)获取基础元素的任意生成器,然后使用.set()、.list()等方法将其转换为集合类型。接着,利用map()或mapEach()进行必要的转换。
  • 清晰的职责分离: DomainContext负责定义基础领域对象的任意生成器,而属性测试类中的@Provide方法则负责基于这些基础生成器组合出更复杂的任意值,供@Property方法消费。

遵循这些原则,将使你的Jqwik属性测试代码更加健壮、易读和符合框架的设计哲学。

以上就是Jqwik中结合@ForAll、@Provide和集合类型正确定义任意生成器的详细内容,更多请关注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号