从 Schema 到 Arbitrary
使用 Arbitrary 生成符合 schema 约束的随机测试数据,并支持转换、过滤器与自定义生成等选项。
Arbitrary.make 函数用于创建与特定 Schema<A, I, R> 相符的随机值。
该函数会返回 fast-check 库中的一个 Arbitrary<A>,
它特别适合用来生成符合所定义 schema 约束的随机测试数据。
示例(为 Schema 生成 Arbitrary 数据)
import { Arbitrary, FastCheck, Schema } from "effect"
// Define a Person schema with constraints
const Person = Schema.Struct({
name: Schema.NonEmptyString,
age: Schema.Int.pipe(Schema.between(1, 80)),
})
// Create an Arbitrary based on the schema
const arb = Arbitrary.make(Person)
// Generate random samples from the Arbitrary
console.log(FastCheck.sample(arb, 2))
/*
Example Output:
[ { name: 'q r', age: 3 }, { name: '&|', age: 6 } ]
*/
想让输出更真实,请参阅自定义 Arbitrary 数据生成一节。
fast-check 的全部 API 都可以通过 FastCheck 导出访问,
你可以在项目中直接使用它的所有功能。
过滤器
生成随机值时,Arbitrary 会尽量遵循 schema 的约束。它会选用最合适的 fast-check primitive(原语),并在该 primitive 支持约束时应用这些约束。
例如,如果你把 age 属性定义为:
Schema.Int.pipe(Schema.between(1, 80))
那么 Arbitrary 生成时会使用:
FastCheck.integer({ min: 1, max: 80 })
来在该范围内生成值。
使用多个过滤器时请注意:相互冲突的过滤器可能导致 Arbitrary 数据生成过程挂起。当这些约束使有效值难以生成或无法生成时,就会出现这种情况。
有关缓解这些问题的指导,请参阅这个讨论。
模式
要为必须匹配某个模式的字符串生成高效的 arbitrary,请使用 Schema.pattern 过滤器,而不是自己编写自定义过滤器:
示例(使用 Schema.pattern 处理模式约束)
import { Schema } from "effect"
// ❌ Without using Schema.pattern (less efficient)
const Bad = Schema.String.pipe(Schema.filter((s) => /^[a-z]+$/.test(s)))
// ✅ Using Schema.pattern (more efficient)
const Good = Schema.String.pipe(Schema.pattern(/^[a-z]+$/))
使用 Schema.pattern 后,arbitrary 生成会依赖 FastCheck.stringMatching(regexp),这更高效,也与所定义的模式直接对应。
当使用多个模式时,它们会被合并成一个 union。例如:
(?:${pattern1})|(?:${pattern2})
这种做法确保在使用 FastCheck.stringMatching 时,所有模式都有相同的机会生成值。
转换与 Arbitrary 生成
生成 Arbitrary 数据时,理解 schema 内部如何处理转换和过滤器很重要:
在转换链中,最后一次转换之前应用的过滤器 在生成 Arbitrary 数据时不会被考虑。
示例(过滤器与转换)
import { Arbitrary, FastCheck, Schema } from "effect"
// Schema with filters before the transformation
const schema1 = Schema.compose(Schema.NonEmptyString, Schema.Trim).pipe(
Schema.maxLength(500),
)
// May produce empty strings due to ignored NonEmpty filter
console.log(FastCheck.sample(Arbitrary.make(schema1), 2))
/*
Example Output:
[ '', '"Ry' ]
*/
// Schema with filters applied after transformations
const schema2 = Schema.Trim.pipe(Schema.nonEmptyString(), Schema.maxLength(500))
// Adheres to all filters, avoiding empty strings
console.log(FastCheck.sample(Arbitrary.make(schema2), 2))
/*
Example Output:
[ ']H+MPXgZKz', 'SNS|waP~\\' ]
*/
解释:
schema1:会考虑Schema.maxLength(500),因为它应用在Schema.Trim转换之后;但会忽略Schema.NonEmptyString,因为它位于转换之前。schema2:完全遵循所有过滤器,因为它们被正确地排在转换之后,从而避免生成不期望的数据。
最佳实践
为确保一致且有效的 Arbitrary 数据生成,请遵循以下准则:
- 先应用过滤器:为初始类型(
I)定义过滤器。 - 应用转换:添加转换来转换数据。
- 应用最后的过滤器:为转换后的类型(
A)使用过滤器。
这样的设置能确保数据处理的每个阶段都精确且定义清晰。
示例(避免混用过滤器与转换)
避免随意组合转换和过滤器:
import { Schema } from "effect"
// Less optimal approach: Mixing transformations and filters
const problematic = Schema.compose(Schema.Lowercase, Schema.Trim)
更推荐结构化的做法:把转换步骤与过滤器应用分开:
示例(更推荐的结构化做法)
import { Schema } from "effect"
// Recommended: Separate transformations and filters
const improved = Schema.transform(
Schema.String,
Schema.String.pipe(Schema.trimmed(), Schema.lowercased()),
{
strict: true,
decode: (s) => s.trim().toLowerCase(),
encode: (s) => s,
},
)
自定义 Arbitrary 数据生成
你可以使用 schema 定义中的 arbitrary 注解来自定义 Arbitrary 数据的生成方式。
示例(自定义 Arbitrary 生成器)
import { Arbitrary, FastCheck, Schema } from "effect"
const Name = Schema.NonEmptyString.annotations({
arbitrary: () => (fc) =>
fc.constantFrom("Alice Johnson", "Dante Howell", "Marta Reyes"),
})
const Age = Schema.Int.pipe(Schema.between(1, 80))
const Person = Schema.Struct({
name: Name,
age: Age,
})
const arb = Arbitrary.make(Person)
console.log(FastCheck.sample(arb, 2))
/*
Example Output:
[ { name: 'Dante Howell', age: 6 }, { name: 'Marta Reyes', age: 53 } ]
*/
该注解可以访问 fast-check 库的完整导出(fc)。
这样你就能返回一个 Arbitrary,精确生成你想要的数据类型。
与假数据生成器集成
在使用 @faker-js/faker 这类 mocking 库时,
你可以把它们与 fast-check 结合,为测试生成逼真的数据。
示例(与 Faker 集成)
import { Arbitrary, FastCheck, Schema } from "effect"
import { faker } from "@faker-js/faker"
const Name = Schema.NonEmptyString.annotations({
arbitrary: () => (fc) =>
fc.constant(null).map(() => {
// Each time the arbitrary is sampled, faker generates a new name
return faker.person.fullName()
}),
})
const Age = Schema.Int.pipe(Schema.between(1, 80))
const Person = Schema.Struct({
name: Name,
age: Age,
})
const arb = Arbitrary.make(Person)
console.log(FastCheck.sample(arb, 2))
/*
Example Output:
[
{ name: 'Henry Dietrich', age: 68 },
{ name: 'Lucas Haag', age: 52 }
]
*/