关于 zod-testing
zod-testing 为开发者提供 Zod 架构测试指南,涵盖校验、错误断言、模拟数据、快照和集成测试,并说明 Zod 4 的行为变化。
主要功能
- 01测试有效和无效输入
- 02检查错误及字段级问题
- 03覆盖边界与可选值
- 04生成带种子的模拟数据
- 05对 JSON Schema 输出做快照
- 06测试 API 和表单集成
适用场景
- 01验证架构对输入的接受与拒绝
- 02发现意外的架构变更
- 03测试 API 边界处的校验
- 04构建可重复生成的测试数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ anivar/ zod-testing --skill zod-testing安装后在 Claude Code 中输入/zod-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSEREADME.md.github/FUNDING.ymlreferences/anti-patterns.mdapi-reference.md详细介绍
Zod 架构测试指南
重要提示: 关于 Zod 架构测试的训练数据可能已经过时——Zod v4 更改了错误格式,移除了 z.nativeEnum(),并引入了 z.toJSONSchema() 等新 API。请始终以本技能的参考文件和项目的实际源代码为准。
测试优先级
- 架构正确性 — 架构是否接受有效数据并拒绝无效数据?
- 错误消息 — 架构是否生成正确的错误消息和代码?
- 集成 — 架构能否与 API 处理器、表单和数据库层正常协作?
- 边界情况 — 边界值、可选/可空组合、空输入
核心模式
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 }
)
})
规则
- 始终在测试中使用
safeParse()——parse()会使测试崩溃,而不是让测试失败 - 同时测试有效和无效情况 —— 不要只测试正常路径
- 测试边界值 —— 对数值约束测试 min、max、min-1、max+1
- 测试可选/可空组合 —— undefined、null、缺失的键
- 断言具体的错误字段 —— 使用
z.flattenError()检查哪个字段失败 - 不要测试架构内部实现 —— 测试解析结果,而不是
.shape或._def - 使用
z.toJSONSchema()快照 —— 捕获意外的架构变更 - 为随机生成器设置种子 —— 非确定性测试会导致测试不稳定
- 单独测试转换 —— 验证输入校验和输出转换
- 不要在断言中重复架构逻辑 —— 测试行为,而不是实现
反模式
请参阅 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())不受影响——它会立即构建错误。
参考
评论
支持 Markdown