zod-contract-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Zod Contract Testing

Zod 契约测试

Test schemas at boundaries — not just happy-path inputs.
Schemas define contracts between systems. A schema that accepts invalid data is a security hole. A schema that rejects valid data is a broken integration. This skill teaches you to systematically test schemas at system boundaries.
When to use: Testing API request/response schemas, WebSocket message validation, database record parsing, external data ingestion, any Zod schema at a system boundary.
When not to use: Internal type assertions, UI component props, type definitions without runtime validation.
在边界处测试schema —— 不要只测试正向路径输入。
Schema定义了系统之间的契约。接受无效数据的schema是安全漏洞,拒绝有效数据的schema则意味着集成故障。本技能将教你如何系统地测试系统边界处的schema。
适用场景:测试API请求/响应schema、WebSocket消息校验、数据库记录解析、外部数据摄入,以及所有位于系统边界的Zod schema。
不适用场景:内部类型断言、UI组件props、无运行时校验的类型定义。

Rationalizations (Do Not Skip)

常见误区(请勿跳过)

RationalizationWhy It's WrongRequired Action
"The type system ensures correctness"TypeScript doesn't exist at runtimeTest Zod parsing with real data
"I tested valid inputs"Invalid inputs cause production errorsTest rejection of invalid inputs
"Refinements are simple"
.refine()
failures are easy to miss
Test BOTH passing and failing cases
"Optional fields are optional"2^N combinations have hidden bugsUse compound state matrix

误区错误原因必要操作
"类型系统能保证正确性"TypeScript在运行时不存在使用真实数据测试Zod解析逻辑
"我已经测试过有效输入了"无效输入会导致生产环境故障测试schema对无效输入的拦截逻辑
"自定义校验规则很简单"
.refine()
的失败场景很容易被遗漏
同时测试校验规则的通过和失败两种场景
"可选字段本来就是可选的"2^N种组合中隐藏着未被发现的bug使用复合状态矩阵覆盖所有组合

What To Protect (Start Here)

防护场景确认(从此处开始)

Before generating schema tests, identify which data integrity decisions apply to your code:
DecisionQuestion to AnswerIf Yes → Use
Invalid data must be rejected with specific errorsWhat malformed inputs could reach this boundary?
testInvalidInput
with
expectedPath
Schema changes must not break old dataIs there serialized/stored data in the old format?
testSchemaEvolution
Version upgrades must be backward compatibleDo multiple schema versions coexist in production?
generateVersionCompatibilityMatrix
Optional field combinations have hidden bugsDoes this schema have 3+ optional fields with interacting refinements?
generateCompoundStateMatrix
Refinements must reject specific threatsWhat invalid-but-plausible input does each
.refine()
guard against?
testRefinement
Do not generate tests for decisions the human hasn't confirmed. A schema test that checks "valid input passes" without naming the threat it guards against is slop — it'll pass even if the schema accepts everything.

在生成schema测试之前,先确认你的代码需要覆盖哪些数据完整性决策:
决策需要回答的问题回答为是 → 使用工具
必须以特定错误拒绝无效数据哪些格式错误的输入可能抵达该边界?传入
expectedPath
参数使用
testInvalidInput
schema变更不能破坏旧数据兼容性是否存在旧格式的序列化/存储数据?
testSchemaEvolution
版本升级必须向后兼容生产环境中是否同时存在多个schema版本?
generateVersionCompatibilityMatrix
可选字段组合存在隐藏bug该schema是否有3个及以上存在交互校验逻辑的可选字段?
generateCompoundStateMatrix
自定义校验规则必须拦截特定风险每个
.refine()
需要防护哪些无效但符合类型的输入?
testRefinement
不要为开发人员未确认的决策生成测试。 一个只检查「有效输入能通过」却不明确防护目标的schema测试是无效的——哪怕schema接受所有输入,这种测试也能通过。

Included Utilities

内置工具函数

typescript
import {
  testValidInput,
  testInvalidInput,
  testSchemaEvolution,
  generateVersionCompatibilityMatrix,
  assertVersionCompatibility,
  testRefinement,
  generateCompoundStateMatrix,
  formatStateMatrix,
  applyCompoundState,
} from './schema-boundary.ts';
typescript
import {
  testValidInput,
  testInvalidInput,
  testSchemaEvolution,
  generateVersionCompatibilityMatrix,
  assertVersionCompatibility,
  testRefinement,
  generateCompoundStateMatrix,
  formatStateMatrix,
  applyCompoundState,
} from './schema-boundary.ts';

Core Workflow

核心工作流

Step 1: Test Valid Inputs

步骤1:测试有效输入

Verify the schema accepts all valid input shapes:
typescript
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
});

it('accepts valid user', () => {
  testValidInput(UserSchema, { name: 'Alice', age: 30 });
  testValidInput(UserSchema, { name: 'Bob' });  // age optional
});
验证schema接受所有合法的输入结构:
typescript
import { z } from 'zod';

const UserSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
});

it('accepts valid user', () => {
  testValidInput(UserSchema, { name: 'Alice', age: 30 });
  testValidInput(UserSchema, { name: 'Bob' });  // age optional
});

Step 2: Test Invalid Inputs

步骤2:测试无效输入

Verify the schema rejects invalid data and errors at the correct path:
typescript
it('rejects missing required field', () => {
  testInvalidInput(UserSchema, {}, 'name');  // Error at 'name' path
});

it('rejects wrong type', () => {
  testInvalidInput(UserSchema, { name: 123 }, 'name');
});

it('rejects unknown fields with strict schema', () => {
  const StrictUserSchema = UserSchema.strict();
  testInvalidInput(StrictUserSchema, { name: 'Alice', role: 'admin' });
});
验证schema拦截无效数据,且错误提示出现在正确的路径上:
typescript
it('rejects missing required field', () => {
  testInvalidInput(UserSchema, {}, 'name');  // Error at 'name' path
});

it('rejects wrong type', () => {
  testInvalidInput(UserSchema, { name: 123 }, 'name');
});

it('rejects unknown fields with strict schema', () => {
  const StrictUserSchema = UserSchema.strict();
  testInvalidInput(StrictUserSchema, { name: 'Alice', role: 'admin' });
});

Step 3a: Test Schema Evolution

步骤3a:测试schema演进

When schemas change, old serialized data must still parse:
typescript
// Old schema: { name: string }
// New schema: { name: string, email?: string }

const NewUserSchema = z.object({
  name: z.string(),
  email: z.string().email().optional(),
});

it('backward compatible with old data', () => {
  const oldData = { name: 'Alice' };  // No email field
  testSchemaEvolution(NewUserSchema, oldData);
});
当schema发生变更时,旧的序列化数据必须仍能被正常解析:
typescript
// Old schema: { name: string }
// New schema: { name: string, email?: string }

const NewUserSchema = z.object({
  name: z.string(),
  email: z.string().email().optional(),
});

it('backward compatible with old data', () => {
  const oldData = { name: 'Alice' };  // No email field
  testSchemaEvolution(NewUserSchema, oldData);
});

Step 3b: Build Version Compatibility Matrix

步骤3b:构建版本兼容性矩阵

For evolving contracts, check
vN -> vN+1
and
vN -> vN+2
explicitly:
typescript
const versions = [
  { version: 'v1', schema: V1Schema, fixtures: [v1Payload] },
  { version: 'v2', schema: V2Schema, fixtures: [v2Payload] },
  { version: 'v3', schema: V3Schema, fixtures: [v3Payload] },
];

const matrix = generateVersionCompatibilityMatrix(versions);

// Enforce only adjacent upgrades (vN -> vN+1)
assertVersionCompatibility(versions, 1);

// Optionally enforce two-hop upgrades (vN -> vN+2)
assertVersionCompatibility(versions, 2);
对于持续演进的契约,显式检查
vN -> vN+1
vN -> vN+2
的兼容性:
typescript
const versions = [
  { version: 'v1', schema: V1Schema, fixtures: [v1Payload] },
  { version: 'v2', schema: V2Schema, fixtures: [v2Payload] },
  { version: 'v3', schema: V3Schema, fixtures: [v3Payload] },
];

const matrix = generateVersionCompatibilityMatrix(versions);

// Enforce only adjacent upgrades (vN -> vN+1)
assertVersionCompatibility(versions, 1);

// Optionally enforce two-hop upgrades (vN -> vN+2)
assertVersionCompatibility(versions, 2);

Step 4: Test Refinements

步骤4:测试自定义校验规则

Every
.refine()
MUST have tests for both passing and failing cases:
typescript
const PositiveNumberSchema = z.object({
  value: z.number().refine(n => n > 0, 'Value must be positive'),
});

it('refinement: positive vs non-positive', () => {
  testRefinement(
    PositiveNumberSchema,
    { value: 10 },           // Passing case
    { value: -5 },           // Failing case
    'must be positive',      // Expected error message
  );
});
每个
.refine()
必须同时覆盖通过和失败两种场景的测试:
typescript
const PositiveNumberSchema = z.object({
  value: z.number().refine(n => n > 0, 'Value must be positive'),
});

it('refinement: positive vs non-positive', () => {
  testRefinement(
    PositiveNumberSchema,
    { value: 10 },           // Passing case
    { value: -5 },           // Failing case
    'must be positive',      // Expected error message
  );
});

Step 5: Generate Compound State Matrix

步骤5:生成复合状态矩阵

For schemas with N optional fields, there are 2^N possible combinations. For 3-4 fields, test exhaustively. For 5+, switch to pairwise coverage (see decision table below):
typescript
// Cell schema: value?, candidates?, isGiven?
// 2^3 = 8 combinations

const matrix = generateCompoundStateMatrix(['value', 'candidates', 'isGiven']);
console.log(formatStateMatrix(matrix));
/*
| # | value | candidates | isGiven | Label |
|---|-------|------------|---------|-------|
| 0 | -     | -          | -       | (empty) |
| 1 | Y     | -          | -       | value |
| 2 | -     | Y          | -       | candidates |
| 3 | Y     | Y          | -       | value + candidates |
| 4 | -     | -          | Y       | isGiven |
| 5 | Y     | -          | Y       | value + isGiven |
| 6 | -     | Y          | Y       | candidates + isGiven |
| 7 | Y     | Y          | Y       | value + candidates + isGiven |
*/
对于有N个可选字段的schema,存在2^N种可能的组合。如果是3-4个字段,可以全量测试;如果是5个及以上,切换为成对覆盖(参考下方决策表):
typescript
// Cell schema: value?, candidates?, isGiven?
// 2^3 = 8 combinations

const matrix = generateCompoundStateMatrix(['value', 'candidates', 'isGiven']);
console.log(formatStateMatrix(matrix));
/*
| # | value | candidates | isGiven | Label |
|---|-------|------------|---------|-------|
| 0 | -     | -          | -       | (empty) |
| 1 | Y     | -          | -       | value |
| 2 | -     | Y          | -       | candidates |
| 3 | Y     | Y          | -       | value + candidates |
| 4 | -     | -          | Y       | isGiven |
| 5 | Y     | -          | Y       | value + isGiven |
| 6 | -     | Y          | Y       | candidates + isGiven |
| 7 | Y     | Y          | Y       | value + candidates + isGiven |
*/

Step 6: Apply Matrix to Schema Tests

步骤6:将矩阵应用到schema测试

Generate test inputs from the matrix:
typescript
const CellSchema = z.object({
  value: z.number().optional(),
  candidates: z.array(z.number()).optional(),
  isGiven: z.boolean().optional(),
}).refine(
  cell => !(cell.isGiven && cell.value === undefined),
  'Given cells must have a digit value',
);

describe('cell schema compound states', () => {
  const matrix = generateCompoundStateMatrix(['value', 'candidates', 'isGiven']);
  const template = { value: 5, candidates: [1, 3, 7], isGiven: true };

  for (const entry of matrix) {
    const input = applyCompoundState(entry, template);
    const shouldFail = input.isGiven && input.value === undefined;

    it(`${entry.label}: ${shouldFail ? 'rejects' : 'accepts'}`, () => {
      if (shouldFail) {
        testInvalidInput(CellSchema, input);  // Object-level .refine() reports at root path
      } else {
        testValidInput(CellSchema, input);
      }
    });
  }
});

基于矩阵生成测试用例输入:
typescript
const CellSchema = z.object({
  value: z.number().optional(),
  candidates: z.array(z.number()).optional(),
  isGiven: z.boolean().optional(),
}).refine(
  cell => !(cell.isGiven && cell.value === undefined),
  'Given cells must have a digit value',
);

describe('cell schema compound states', () => {
  const matrix = generateCompoundStateMatrix(['value', 'candidates', 'isGiven']);
  const template = { value: 5, candidates: [1, 3, 7], isGiven: true };

  for (const entry of matrix) {
    const input = applyCompoundState(entry, template);
    const shouldFail = input.isGiven && input.value === undefined;

    it(`${entry.label}: ${shouldFail ? 'rejects' : 'accepts'}`, () => {
      if (shouldFail) {
        testInvalidInput(CellSchema, input);  // Object-level .refine() reports at root path
      } else {
        testValidInput(CellSchema, input);
      }
    });
  }
});

Compound State Matrix Decision

复合状态矩阵决策表

Optional FieldsCombinationsTesting Approach
0-21-4Enumerate manually
38Use matrix, manageable
416Use matrix, essential
5+32+Switch to pairwise-test-coverage — covers all field pairs in near-minimal test cases

可选字段数量组合数测试方案
0-21-4手动枚举
38使用矩阵,工作量可控
416使用矩阵,非常有必要
5+32+切换为pairwise-test-coverage——用接近最少的测试用例覆盖所有字段对组合

Violation Rules

违规规则

missing_invalid_input_test

missing_invalid_input_test

Every Zod schema MUST have tests for invalid inputs, not just valid ones. Severity: must-fail
每个Zod schema必须包含无效输入测试,不能只测试有效输入。 严重级别:必须拦截

missing_refinement_coverage

missing_refinement_coverage

Every
.refine()
or
.superRefine()
MUST have tests for both the passing AND failing case. Severity: must-fail
每个
.refine()
.superRefine()
必须同时包含通过和失败场景的测试。 严重级别:必须拦截

missing_compound_state_test

missing_compound_state_test

Schemas with 3+ optional fields SHOULD use compound state matrix. For 5+ fields, switch to pairwise-test-coverage rather than testing all 2^N individually. Severity: should-fail
包含3个及以上可选字段的schema建议使用复合状态矩阵。对于5个及以上字段的schema,切换为pairwise-test-coverage,不要逐个测试全部2^N种组合。 严重级别:建议拦截

schema_not_at_boundary

schema_not_at_boundary

Zod parsing MUST happen at system boundaries (API handlers, WebSocket messages, database reads), not inside business logic. Severity: should-fail
Zod解析必须在系统边界处执行(API handler、WebSocket消息处理、数据库读取),不能放在业务逻辑内部。 严重级别:建议拦截

type_assertion_instead_of_parse

type_assertion_instead_of_parse

Use
Schema.parse()
or
Schema.safeParse()
, NEVER
as Type
casts for external data. Severity: must-fail

对于外部数据,使用
Schema.parse()
Schema.safeParse()
,绝对不要使用
as Type
类型断言。 严重级别:必须拦截

Companion Skills

关联技能

This skill teaches testing methodology, not Zod API usage. For broader methodology:
  • Search
    zod
    on skills.sh for schema authoring, transforms, error handling, and framework integrations
  • Our utilities work with any Zod version (v3, v4) via the
    ZodLikeSchema
    interface — no version lock-in
  • Schemas with 5+ optional fields produce 32+ combinations — use pairwise-test-coverage for near-minimal coverage of all field pairs
  • Schema parsing at boundaries often logs errors — use observability-testing to assert structured log output on validation failures
  • Schema evolution testing pairs with resilience — use fault-injection-testing for retry and circuit breaker testing around schema-validated endpoints

本技能教授的是测试方法论,而非Zod API使用方法。如需了解更广泛的方法论:
  • skills.sh搜索
    zod
    可查看schema编写、转换、错误处理以及框架集成相关内容
  • 我们的工具通过
    ZodLikeSchema
    接口兼容所有Zod版本(v3、v4)——无版本绑定
  • 包含5个及以上可选字段的schema会产生32种以上组合——使用pairwise-test-coverage可以用接近最少的用例覆盖所有字段对
  • 边界处的schema解析通常会记录错误日志——使用observability-testing可以验证校验失败时的结构化日志输出
  • schema演进测试和弹性能力适配——使用fault-injection-testing可以测试schema校验接口的重试和熔断逻辑

Quick Reference

快速参考

UtilityWhenExample
testValidInput
Verify acceptance
testValidInput(schema, { name: 'Alice' })
testInvalidInput
Verify rejection
testInvalidInput(schema, {}, 'name')
testSchemaEvolution
Backward compat
testSchemaEvolution(newSchema, oldData)
generateVersionCompatibilityMatrix
Version matrix
generateVersionCompatibilityMatrix(versions)
assertVersionCompatibility
Enforce vN->vN+K
assertVersionCompatibility(versions, 2)
testRefinement
Pass + fail
testRefinement(schema, passing, failing, message)
generateCompoundStateMatrix
Optional fields
generateCompoundStateMatrix(['a', 'b', 'c'])
applyCompoundState
Generate input
applyCompoundState(entry, template)
See patterns.md for Zod-specific patterns, boundary testing methodology, and integration with TypeScript strict mode.
工具函数使用场景示例
testValidInput
验证输入被接受
testValidInput(schema, { name: 'Alice' })
testInvalidInput
验证输入被拦截
testInvalidInput(schema, {}, 'name')
testSchemaEvolution
向后兼容性测试
testSchemaEvolution(newSchema, oldData)
generateVersionCompatibilityMatrix
生成版本兼容性矩阵
generateVersionCompatibilityMatrix(versions)
assertVersionCompatibility
强制vN->vN+K兼容
assertVersionCompatibility(versions, 2)
testRefinement
校验规则通/断场景测试
testRefinement(schema, passing, failing, message)
generateCompoundStateMatrix
可选字段组合覆盖
generateCompoundStateMatrix(['a', 'b', 'c'])
applyCompoundState
生成测试输入
applyCompoundState(entry, template)
查看patterns.md了解Zod专属模式、边界测试方法论以及与TypeScript严格模式的集成方法。