0

0

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

心靈之曲

心靈之曲

发布时间:2025-10-10 15:05:00

|

821人浏览过

|

来源于php中文网

原创

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

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

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

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

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

// 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 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> namesToParse(
    @ForAll @Size(min = 1, max = 4) Set names) {
    // ... code here to convert Set to Set
    // This method is intended to provide Arbitrary>
    return Arbitrjust.just(names.stream()
                                .map(n -> n.first + " " + n.last)
                                .collect(Collectors.toSet()));
  }

  @Property
  public void namesAreParsed(@ForAll("namesToParse") Set names) {
    // ... use the generated Set here
  }
}

运行上述代码,Jqwik会抛出CannotFindArbitraryException,指出无法为namesToParse方法中的Set参数找到任意生成器。这通常是由于对@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的第一步。

KAIZAN.ai
KAIZAN.ai

使用AI来改善客户服体验,提高忠诚度

下载
// 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> namesToParse(
    @ForAll @Size(min = 1, max = 4) Set 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 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
  2. 转换为集合类型的任意生成器: 在获取到基础元素的Arbitrary之后,我们可以使用.set()、.list()等方法将其转换为集合类型的任意生成器。同时,可以通过ofMinSize()和ofMaxSize()等方法来限制集合的大小。
  3. 对集合进行映射或转换: 得到SetArbitrary之后,我们可以使用其map()方法对生成的Set进行整体转换,或者使用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 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> namesToParse() {
    // 1. 获取Name类型的任意生成器,Jqwik会从NameDomain中查找
    // 2. 将其转换为Set的任意生成器,并指定大小范围
    SetArbitrary namesArbitrary = Arbitraries.defaultFor(Name.class)
                                                    .set().ofMinSize(1).ofMaxSize(4);

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

  @Property
  public void namesAreParsed(@ForAll("namesToParse") Set 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属性测试代码更加健壮、易读和符合框架的设计哲学。

相关专题

更多
string转int
string转int

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

315

2023.08.02

class在c语言中的意思
class在c语言中的意思

在C语言中,"class" 是一个关键字,用于定义一个类。想了解更多class的相关内容,可以阅读本专题下面的文章。

465

2024.01.03

python中class的含义
python中class的含义

本专题整合了python中class的相关内容,阅读专题下面的文章了解更多详细内容。

12

2025.12.06

golang map内存释放
golang map内存释放

本专题整合了golang map内存相关教程,阅读专题下面的文章了解更多相关内容。

75

2025.09.05

golang map相关教程
golang map相关教程

本专题整合了golang map相关教程,阅读专题下面的文章了解更多详细内容。

32

2025.11.16

golang map原理
golang map原理

本专题整合了golang map相关内容,阅读专题下面的文章了解更多详细内容。

59

2025.11.17

java判断map相关教程
java判断map相关教程

本专题整合了java判断map相关教程,阅读专题下面的文章了解更多详细内容。

37

2025.11.27

Golang gRPC 服务开发与Protobuf实战
Golang gRPC 服务开发与Protobuf实战

本专题系统讲解 Golang 在 gRPC 服务开发中的完整实践,涵盖 Protobuf 定义与代码生成、gRPC 服务端与客户端实现、流式 RPC(Unary/Server/Client/Bidirectional)、错误处理、拦截器、中间件以及与 HTTP/REST 的对接方案。通过实际案例,帮助学习者掌握 使用 Go 构建高性能、强类型、可扩展的 RPC 服务体系,适用于微服务与内部系统通信场景。

4

2026.01.15

公务员递补名单公布时间 公务员递补要求
公务员递补名单公布时间 公务员递补要求

公务员递补名单公布时间不固定,通常在面试前,由招录单位(如国家知识产权局、海关等)发布,依据是原入围考生放弃资格,会按笔试成绩从高到低递补,递补考生需按公告要求限时确认并提交材料,及时参加面试/体检等后续环节。要求核心是按招录单位公告及时响应、提交材料(确认书、资格复审材料)并准时参加面试。

23

2026.01.15

热门下载

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

精品课程

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

共23课时 | 2.5万人学习

C# 教程
C# 教程

共94课时 | 6.8万人学习

Java 教程
Java 教程

共578课时 | 46.4万人学习

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

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