已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

从 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 数据生成一节。

Access FastCheck API

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 })

来在该范围内生成值。

Avoiding Conflicts in Filters

使用多个过滤器时请注意:相互冲突的过滤器可能导致 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 内部如何处理转换和过滤器很重要:

Filters Ignored

在转换链中,最后一次转换之前应用的过滤器 在生成 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 数据生成,请遵循以下准则:

  1. 先应用过滤器:为初始类型(I)定义过滤器。
  2. 应用转换:添加转换来转换数据。
  3. 应用最后的过滤器:为转换后的类型(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 }
]
*/