mcpField
Z
zod-testing

zod-testing

github.com/anivar测试与质量239 次安装2 周前更新
代码仓库

关于 zod-testing

zod-testing 为开发者提供 Zod 架构测试指南,涵盖校验、错误断言、模拟数据、快照和集成测试,并说明 Zod 4 的行为变化。

主要功能

  1. 01测试有效和无效输入
  2. 02检查错误及字段级问题
  3. 03覆盖边界与可选值
  4. 04生成带种子的模拟数据
  5. 05对 JSON Schema 输出做快照
  6. 06测试 API 和表单集成

适用场景

  1. 01验证架构对输入的接受与拒绝
  2. 02发现意外的架构变更
  3. 03测试 API 边界处的校验
  4. 04构建可重复生成的测试数据

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/anivar/zod-testing --skill zod-testing

安装后在 Claude Code 中输入/zod-testing确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

LICENSE
README.md
.github/
FUNDING.yml
references/
anti-patterns.md
api-reference.md

详细介绍

Zod 架构测试指南

重要提示: 关于 Zod 架构测试的训练数据可能已经过时——Zod v4 更改了错误格式,移除了 z.nativeEnum(),并引入了 z.toJSONSchema() 等新 API。请始终以本技能的参考文件和项目的实际源代码为准。

测试优先级

  1. 架构正确性 — 架构是否接受有效数据并拒绝无效数据?
  2. 错误消息 — 架构是否生成正确的错误消息和代码?
  3. 集成 — 架构能否与 API 处理器、表单和数据库层正常协作?
  4. 边界情况 — 边界值、可选/可空组合、空输入

核心模式

import { describe, it, expect } from "vitest" // or jest
import { z } from "zod"

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.email(),
  age: z.number().min(0).max(150),
})

describe("UserSchema", () => {
  it("accepts valid data", () => {
    const result = UserSchema.safeParse({
      name: "Alice",
      email: "[email protected]",
      age: 30,
    })
    expect(result.success).toBe(true)
  })

  it("rejects missing required fields", () => {
    const result = UserSchema.safeParse({})
    expect(result.success).toBe(false)
    if (!result.success) {
      const flat = z.flattenError(result.error)
      expect(flat.fieldErrors.name).toBeDefined()
      expect(flat.fieldErrors.email).toBeDefined()
    }
  })

  it("rejects invalid email", () => {
    const result = UserSchema.safeParse({
      name: "Alice",
      email: "not-an-email",
      age: 30,
    })
    expect(result.success).toBe(false)
  })

  it("rejects negative age", () => {
    const result = UserSchema.safeParse({
      name: "Alice",
      email: "[email protected]",
      age: -1,
    })
    expect(result.success).toBe(false)
  })
})

测试方法

方法用途适用场景
检查 safeParse() 结果架构正确性默认方式——始终在测试中使用 safeParse
断言 z.flattenError()错误消息测试验证特定字段的错误
z.toJSONSchema() 快照架构形状测试检测意外的架构变更
模拟数据生成创建测试夹具需要有效/随机化的测试数据
基于属性的测试模糊测试架构必须处理任意有效输入
结构测试架构设计验证架构仅在边界处导入
漂移检测回归测试通过 JSON Schema 快照捕获意外的架构变更

架构正确性测试

在测试中始终使用 safeParse()
// GOOD: test doesn't crash — asserts on result
const result = schema.safeParse(invalidData)
expect(result.success).toBe(false)

// BAD: test crashes instead of failing
expect(() => schema.parse(invalidData)).toThrow()
// If schema changes and starts accepting, this still passes
同时测试接受和拒绝的情况
describe("EmailSchema", () => {
  const valid = ["[email protected]", "[email protected]", "[email protected]"]
  const invalid = ["", "not-email", "@missing.com", "user@", "user @space.com"]

  it.each(valid)("accepts %s", (email) => {
    expect(z.email().safeParse(email).success).toBe(true)
  })

  it.each(invalid)("rejects %s", (email) => {
    expect(z.email().safeParse(email).success).toBe(false)
  })
})
测试边界值
const AgeSchema = z.number().min(0).max(150)

it("accepts minimum boundary", () => {
  expect(AgeSchema.safeParse(0).success).toBe(true)
})

it("accepts maximum boundary", () => {
  expect(AgeSchema.safeParse(150).success).toBe(true)
})

it("rejects below minimum", () => {
  expect(AgeSchema.safeParse(-1).success).toBe(false)
})

it("rejects above maximum", () => {
  expect(AgeSchema.safeParse(151).success).toBe(false)
})

错误断言模式

断言特定字段的错误
it("shows correct error for invalid email", () => {
  const result = UserSchema.safeParse({ name: "Alice", email: "bad", age: 30 })
  expect(result.success).toBe(false)
  if (!result.success) {
    const flat = z.flattenError(result.error)
    expect(flat.fieldErrors.email).toBeDefined()
    expect(flat.fieldErrors.email![0]).toContain("email")
  }
})
断言错误代码
it("produces correct error code", () => {
  const result = z.number().safeParse("not a number")
  expect(result.success).toBe(false)
  if (!result.success) {
    expect(result.error.issues[0].code).toBe("invalid_type")
  }
})
断言自定义错误消息
const Schema = z.string({ error: "Name is required" }).min(1, "Name cannot be empty")

it("shows custom error for missing field", () => {
  const result = Schema.safeParse(undefined)
  expect(result.success).toBe(false)
  if (!result.success) {
    expect(result.error.issues[0].message).toBe("Name is required")
  }
})

模拟数据生成

使用 zod-schema-faker
import { install, fake } from "zod-schema-faker"
import { z } from "zod"

install(z) // call once in test setup

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.email(),
  age: z.number().min(0).max(150),
})

it("schema accepts generated data", () => {
  const mockUser = fake(UserSchema)
  expect(UserSchema.safeParse(mockUser).success).toBe(true)
})
为确定性测试设置种子
import { seed, fake } from "zod-schema-faker"

beforeEach(() => {
  seed(12345) // deterministic output
})

it("generates consistent mock data", () => {
  const user = fake(UserSchema)
  expect(user.name).toBeDefined()
})

使用 JSON Schema 进行快照测试

it("schema shape has not changed", () => {
  const jsonSchema = z.toJSONSchema(UserSchema)
  expect(jsonSchema).toMatchSnapshot()
})

这有助于在代码审查中捕获意外的架构变更。快照展示了 Zod 架构的 JSON Schema 表示形式。

集成测试

API 处理器测试
it("API rejects invalid request body", async () => {
  const response = await request(app)
    .post("/api/users")
    .send({ name: "", email: "invalid" })
    .expect(400)

  expect(response.body.errors).toBeDefined()
  expect(response.body.errors.fieldErrors.email).toBeDefined()
})
表单验证测试
it("form shows validation errors", () => {
  const result = FormSchema.safeParse(formData)
  if (!result.success) {
    const errors = z.flattenError(result.error)
    // Pass errors to form library
    expect(errors.fieldErrors).toHaveProperty("email")
  }
})

基于属性的测试

import fc from "fast-check"
import { fake } from "zod-schema-faker"

it("schema always accepts its own generated data", () => {
  fc.assert(
    fc.property(fc.constant(null), () => {
      const data = fake(UserSchema)
      expect(UserSchema.safeParse(data).success).toBe(true)
    }),
    { numRuns: 100 }
  )
})

规则

  1. 始终在测试中使用 safeParse() —— parse() 会使测试崩溃,而不是让测试失败
  2. 同时测试有效和无效情况 —— 不要只测试正常路径
  3. 测试边界值 —— 对数值约束测试 min、max、min-1、max+1
  4. 测试可选/可空组合 —— undefined、null、缺失的键
  5. 断言具体的错误字段 —— 使用 z.flattenError() 检查哪个字段失败
  6. 不要测试架构内部实现 —— 测试解析结果,而不是 .shape 或 ._def
  7. 使用 z.toJSONSchema() 快照 —— 捕获意外的架构变更
  8. 为随机生成器设置种子 —— 非确定性测试会导致测试不稳定
  9. 单独测试转换 —— 验证输入校验和输出转换
  10. 不要在断言中重复架构逻辑 —— 测试行为,而不是实现

反模式

请参阅 references/anti-patterns.md,查看以下内容的错误/正确示例:

  • 测试架构内部实现,而不是行为
  • 不测试错误路径
  • 在测试中使用 parse()(会导致崩溃,而不是失败)
  • 不测试边界值
  • 硬编码模拟数据,而不是生成数据
  • 对原始 ZodError 而不是格式化后的输出进行快照测试
  • 不在边界处测试(架构测试通过,但处理器没有执行验证)
  • 没有快照回归测试(字段移除后未被察觉)
  • 只测试架构形状,却不测试错误是否可观测(从不对 flattenError 进行断言)
  • 没有漂移检测工作流(架构变更未经机械化审查就被合并)

测试中的 Zod 4.6

只需布尔值时,使用 .validate() 进行断言

.validate() 会返回类型守卫,并在遇到第一个问题时立即短路。对于纯布尔断言,它比 .safeParse().success 开销更低,尤其是输入无效时(编译后最高快 35 倍,未编译时快 5.9 倍)。

const Player = z.object({ username: z.string(), xp: z.number() })

it('accepts a valid player', () => {
  expect(Player.validate({ username: 'john', xp: 3 })).toBe(true)
})

it('rejects an invalid player', () => {
  expect(Player.validate({ username: 42, xp: 3 })).toBe(false)
})

如果测试需要断言解析后的值或问题列表,请继续使用 .safeParse()。当测试套件中频繁使用某些 schema 时,使用 z.compile() 加上 .validate() 可以消除大部分断言开销。

错误映射是惰性求值的:断言消息前先读取 result.error

从 4.6 版开始,错误映射(全局、区域设置、每个 schema 的 error)会在首次读取 result.error 时求值,而不是在解析时求值。如果在解析和断言之间更改 z.config(),测试会读取到较新的配置:

const result = schema.safeParse(12)
z.config(z.locales.fr())
expect(result.error.issues[0].message).toBe('…French…') // French in 4.6, English in 4.5

解析和断言应使用同一配置状态;不要认为消息是在解析时计算的。抛出错误的解析(.parse())不受影响——它会立即构建错误。

参考

  • API 参考 — 测试模式、断言辅助工具、模拟数据生成
  • 反模式 — 应避免的常见测试错误

评论

支持 Markdown