
本文旨在解决jqwik测试框架中,当尝试在`@provide`方法中使用`@forall`注解与集合类型时常遇到的`cannotfindarbitraryexception`。我们将深入探讨`@domain`注解的正确作用域,并展示如何优雅地构建基于集合的任意生成器,避免不必要的扁平化映射,从而实现更灵活和高效的属性测试。
在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注解用于指定属性测试方法(即被@Property注解的方法)或其包含的测试类(Test Class)的领域上下文。它告诉Jqwik在当前作用域内,当遇到需要生成特定类型的任意值时,应该从哪个DomainContext中查找相应的@Provide方法。
关键点在于:@Domain不应直接应用于一个自身带有@ForAll参数的@Provide方法。 @Provide方法本身是用于定义和提供Arbitrary实例的,而不是直接消费领域上下文中的任意值。当一个@Provide方法需要依赖其他Arbitrary来构建自己的Arbitrary时,它应该通过Jqwik的Arbitraries工具类或组合器来显式地获取和操作这些Arbitrary,而不是通过其方法参数上的@ForAll注解。
因此,@Domain注解的正确放置方式有以下两种:
根据我们的示例,将@Domain(NameDomain.class)从namesToParse方法上移除,并将其放置在NameProperties类上,是解决CannotFindArbitraryException的第一步。
// 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) {
// ...
}
}即使@Domain注解放置正确,原始namesToParse方法的设计仍然不符合Jqwik的最佳实践。当@Provide方法带有@ForAll参数时,Jqwik会将其视为一个“扁平化映射”(flat mapping)操作,这通常不是我们希望构建一个集合类型Arbitrary时的行为。我们通常希望的是直接在@Provide方法内部组合或转换Arbitrary,而不是依赖于方法参数的注入。
推荐的解决方案是在@Provide方法内部直接构建目标集合类型的Arbitrary,利用Jqwik强大的Arbitraries工具类和链式调用。 这样可以更清晰、更灵活地定义任意生成逻辑。
以下是优化namesToParse方法的步骤:
以下是优化后的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方法时遇到的常见问题。以下是关键的注意事项和最佳实践:
遵循这些原则,将使你的Jqwik属性测试代码更加健壮、易读和符合框架的设计哲学。
以上就是Jqwik中结合@ForAll、@Provide和集合类型正确定义任意生成器的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号