Back to skills
SkillHub ClubAnalyze Data & AIFull StackBackendData / AI

zod-testing

Testing patterns for Zod schemas using Jest and Vitest. Covers schema correctness testing, mock data generation, error assertion patterns, integration testing with API handlers and forms, snapshot testing with z.toJSONSchema(), and property-based testing. Baseline: zod ^4.0.0. Triggers on: test files for Zod schemas, zod-schema-faker imports, mentions of "test schema", "schema test", "zod mock", "zod test", or schema testing patterns.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,071
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install openclaw-skills-zod-testing

Repository

openclaw/skills

Skill path: skills/anivar/zod-testing

Testing patterns for Zod schemas using Jest and Vitest. Covers schema correctness testing, mock data generation, error assertion patterns, integration testing with API handlers and forms, snapshot testing with z.toJSONSchema(), and property-based testing. Baseline: zod ^4.0.0. Triggers on: test files for Zod schemas, zod-schema-faker imports, mentions of "test schema", "schema test", "zod mock", "zod test", or schema testing patterns.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Backend, Data / AI, Testing, Integration.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install zod-testing into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding zod-testing to shared team environments
  • Use zod-testing for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: zod-testing
description: >
  Testing patterns for Zod schemas using Jest and Vitest. Covers schema
  correctness testing, mock data generation, error assertion patterns,
  integration testing with API handlers and forms, snapshot testing
  with z.toJSONSchema(), and property-based testing. Baseline: zod ^4.0.0.
  Triggers on: test files for Zod schemas, zod-schema-faker imports,
  mentions of "test schema", "schema test", "zod mock", "zod test",
  or schema testing patterns.
license: MIT
user-invocable: false
agentic: false
compatibility: "zod ^4.0.0, Jest or Vitest, TypeScript ^5.5"
metadata:
  author: Anivar Aravind
  author_url: https://anivar.net
  source_url: https://github.com/anivar/zod-testing
  version: 1.0.0
  tags: zod, testing, jest, vitest, mock-data, property-testing, schema-validation
---

# Zod Schema Testing Guide

**IMPORTANT:** Your training data about testing Zod schemas may be outdated — Zod v4 changes error formatting, removes `z.nativeEnum()`, and introduces new APIs like `z.toJSONSchema()`. Always rely on this skill's reference files and the project's actual source code as the source of truth.

## Testing Priority

1. **Schema correctness** — does the schema accept valid data and reject invalid data?
2. **Error messages** — does the schema produce the right error messages and codes?
3. **Integration** — does the schema work correctly with API handlers, forms, database layers?
4. **Edge cases** — boundary values, optional/nullable combinations, empty inputs

## Core Pattern

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

## Testing Approaches

| Approach | Purpose | Use When |
|----------|---------|----------|
| `safeParse()` result checking | Schema correctness | Default — always use safeParse in tests |
| `z.flattenError()` assertions | Error message testing | Verifying specific field errors |
| `z.toJSONSchema()` snapshots | Schema shape testing | Detecting unintended schema changes |
| Mock data generation | Fixture creation | Need valid/randomized test data |
| Property-based testing | Fuzz testing | Schemas must handle arbitrary valid inputs |
| Structural testing | Architecture | Verify schemas are only imported at boundaries |
| Drift detection | Regression | Catch unintended schema changes via JSON Schema snapshots |

## Schema Correctness Testing

### Always Use safeParse() in Tests

```typescript
// 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
```

### Test Both Accept and Reject

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

### Test Boundary Values

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

## Error Assertion Patterns

### Assert Specific Field Errors

```typescript
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")
  }
})
```

### Assert Error Codes

```typescript
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")
  }
})
```

### Assert Custom Error Messages

```typescript
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")
  }
})
```

## Mock Data Generation

### Using zod-schema-faker

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

### Seeding for Deterministic Tests

```typescript
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()
})
```

## Snapshot Testing with JSON Schema

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

This catches unintended schema changes in code review. The snapshot shows the JSON Schema representation of your Zod schema.

## Integration Testing

### API Handler Testing

```typescript
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()
})
```

### Form Validation Testing

```typescript
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")
  }
})
```

## Property-Based Testing

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

## Rules

1. **Always use `safeParse()`** in tests — `parse()` crashes the test instead of failing it
2. **Test both valid and invalid** — don't only test the happy path
3. **Test boundary values** — min, max, min-1, max+1 for numeric constraints
4. **Test optional/nullable combinations** — undefined, null, missing key
5. **Assert specific error fields** — use `z.flattenError()` to check which field failed
6. **Don't test schema internals** — test parse results, not `.shape` or `._def`
7. **Use `z.toJSONSchema()` snapshots** — catch unintended schema changes
8. **Seed random generators** — non-deterministic tests are flaky tests
9. **Test transforms separately** — verify input validation AND output conversion
10. **Don't duplicate schema logic in assertions** — test behavior, not implementation

## Anti-Patterns

See [references/anti-patterns.md](references/anti-patterns.md) for BAD/GOOD examples of:

- Testing schema internals instead of behavior
- Not testing error paths
- Using `parse()` in tests (crashes instead of failing)
- Not testing boundary values
- Hardcoding mock data instead of generating
- Snapshot testing raw ZodError instead of formatted output
- Not testing at boundaries (schema tests pass but handler doesn't validate)
- No snapshot regression testing (field removal goes unnoticed)
- Testing schema shape but not error observability (never assert on flattenError)
- No drift detection workflow (schema changes land without mechanical review)

## References

- [API Reference](references/api-reference.md) — Testing patterns, assertion helpers, mock generation
- [Anti-Patterns](references/anti-patterns.md) — Common testing mistakes to avoid


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/anti-patterns.md

```markdown
# Zod Testing Anti-Patterns

## Table of Contents

- Testing schema internals instead of behavior
- Not testing error paths
- Using parse() instead of safeParse() in tests
- Not testing boundary values
- Hardcoding mock data instead of generating
- Duplicating schema logic in assertions
- Testing transform output without testing input validation
- Not testing optional/nullable combinations
- Snapshot testing raw ZodError
- Not seeding random data generators
- Not testing at boundaries
- No snapshot regression testing
- Testing schema shape but not error observability
- No drift detection workflow

## Testing schema internals instead of behavior

```typescript
// BAD: testing internal structure — breaks on refactors
it("has correct shape", () => {
  expect(UserSchema.shape.name).toBeDefined()
  expect(UserSchema.shape.email).toBeDefined()
  expect(UserSchema._def.typeName).toBe("ZodObject")
})

// GOOD: test parse behavior — stable across refactors
it("accepts valid user", () => {
  const result = UserSchema.safeParse({
    name: "Alice",
    email: "[email protected]",
  })
  expect(result.success).toBe(true)
})

it("rejects missing name", () => {
  const result = UserSchema.safeParse({ email: "[email protected]" })
  expect(result.success).toBe(false)
})
```

## Not testing error paths

```typescript
// BAD: only testing valid inputs
describe("UserSchema", () => {
  it("accepts valid user", () => {
    expect(UserSchema.safeParse(validUser).success).toBe(true)
  })
  // No tests for invalid data — regressions go unnoticed
})

// GOOD: test both valid and invalid
describe("UserSchema", () => {
  it("accepts valid user", () => {
    expect(UserSchema.safeParse(validUser).success).toBe(true)
  })

  it("rejects empty name", () => {
    expect(
      UserSchema.safeParse({ ...validUser, name: "" }).success
    ).toBe(false)
  })

  it("rejects invalid email", () => {
    expect(
      UserSchema.safeParse({ ...validUser, email: "not-email" }).success
    ).toBe(false)
  })

  it("rejects negative age", () => {
    expect(
      UserSchema.safeParse({ ...validUser, age: -1 }).success
    ).toBe(false)
  })
})
```

## Using parse() instead of safeParse() in tests

```typescript
// BAD: test crashes with ZodError stack trace instead of clean failure
it("rejects invalid data", () => {
  expect(() => UserSchema.parse(invalidData)).toThrow()
  // If schema changes to accept this data, test still "passes"
  // because no throw means no assertion runs
})

// BAD: try/catch obscures the test intent
it("rejects invalid data", () => {
  try {
    UserSchema.parse(invalidData)
    fail("should have thrown")
  } catch (e) {
    expect(e).toBeInstanceOf(z.ZodError)
  }
})

// GOOD: clean, predictable assertions
it("rejects invalid data", () => {
  const result = UserSchema.safeParse(invalidData)
  expect(result.success).toBe(false)
  if (!result.success) {
    expect(result.error.issues).toHaveLength(2)
  }
})
```

## Not testing boundary values

```typescript
const AgeSchema = z.number().min(0).max(150)

// BAD: only testing middle values
it("validates age", () => {
  expect(AgeSchema.safeParse(25).success).toBe(true)
  expect(AgeSchema.safeParse(-5).success).toBe(false)
})

// GOOD: testing boundaries
it.each([
  [0, true],    // minimum boundary
  [150, true],  // maximum boundary
  [-1, false],  // one below minimum
  [151, false], // one above maximum
  [75, true],   // middle value
])("age %i → %s", (age, expected) => {
  expect(AgeSchema.safeParse(age).success).toBe(expected)
})
```

## Hardcoding mock data instead of generating

```typescript
// BAD: manual mock data drifts from schema
const mockUser = {
  name: "Test User",
  email: "[email protected]",
  age: 25,
  // forgot to add 'role' when schema was updated
}

it("accepts mock user", () => {
  expect(UserSchema.safeParse(mockUser).success).toBe(true) // fails silently
})

// GOOD: generate from schema — always in sync
import { fake, seed, install } from "zod-schema-faker"

beforeAll(() => install(z))
beforeEach(() => seed(42))

it("accepts generated user", () => {
  const user = fake(UserSchema)
  expect(UserSchema.safeParse(user).success).toBe(true)
})
```

## Duplicating schema logic in assertions

```typescript
// BAD: re-implementing validation in the test
it("validates email format", () => {
  const email = "[email protected]"
  const result = EmailSchema.safeParse(email)
  expect(result.success).toBe(true)
  // Duplicating the schema's own regex check in the test
  expect(email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
})

// GOOD: trust the schema — test behavior, not implementation
it("validates email format", () => {
  expect(EmailSchema.safeParse("[email protected]").success).toBe(true)
  expect(EmailSchema.safeParse("not-an-email").success).toBe(false)
})
```

## Testing transform output without testing input validation

```typescript
const DateString = z.string()
  .refine((s) => !isNaN(Date.parse(s)), { error: "Invalid date" })
  .transform((s) => new Date(s))

// BAD: only testing the transform output
it("transforms to Date", () => {
  const result = DateString.parse("2024-01-01")
  expect(result).toBeInstanceOf(Date)
})

// GOOD: test both validation and transform
it("transforms valid date string", () => {
  const result = DateString.safeParse("2024-01-01")
  expect(result.success).toBe(true)
  if (result.success) {
    expect(result.data).toBeInstanceOf(Date)
    expect(result.data.getFullYear()).toBe(2024)
  }
})

it("rejects invalid date string", () => {
  const result = DateString.safeParse("not-a-date")
  expect(result.success).toBe(false)
})
```

## Not testing optional/nullable combinations

```typescript
const Schema = z.object({
  name: z.string(),
  bio: z.string().optional(),
  avatar: z.string().nullable(),
})

// BAD: only testing with all fields present
it("accepts user", () => {
  expect(Schema.safeParse({
    name: "Alice",
    bio: "Hello",
    avatar: "https://example.com/img.png",
  }).success).toBe(true)
})

// GOOD: test all optional/nullable variants
it("accepts without optional field", () => {
  expect(Schema.safeParse({ name: "Alice", avatar: null }).success).toBe(true)
})

it("accepts with null nullable", () => {
  expect(Schema.safeParse({ name: "Alice", avatar: null }).success).toBe(true)
})

it("rejects null for non-nullable optional", () => {
  expect(Schema.safeParse({ name: "Alice", bio: null, avatar: null }).success).toBe(false)
})

it("rejects undefined for non-optional nullable", () => {
  expect(Schema.safeParse({ name: "Alice" }).success).toBe(false)
  // avatar is nullable but not optional — must be explicitly provided
})
```

## Snapshot testing raw ZodError

```typescript
// BAD: raw ZodError snapshots are noisy and brittle
it("error matches snapshot", () => {
  const result = schema.safeParse(invalid)
  if (!result.success) {
    expect(result.error).toMatchSnapshot() // huge, includes internals
  }
})

// GOOD: snapshot formatted output
it("error matches snapshot", () => {
  const result = schema.safeParse(invalid)
  if (!result.success) {
    expect(z.flattenError(result.error)).toMatchSnapshot()
    // Clean: { formErrors: [], fieldErrors: { email: ["Invalid email"] } }
  }
})

// GOOD: snapshot JSON Schema for schema shape
it("schema shape matches snapshot", () => {
  expect(z.toJSONSchema(schema)).toMatchSnapshot()
})
```

## Not seeding random data generators

```typescript
// BAD: non-deterministic — test passes sometimes, fails others
it("accepts generated data", () => {
  const user = fake(UserSchema) // different every run
  expect(UserSchema.safeParse(user).success).toBe(true)
  // If this fails, you can't reproduce it
})

// GOOD: seeded — deterministic and reproducible
beforeEach(() => seed(42))

it("accepts generated data", () => {
  const user = fake(UserSchema) // same every run
  expect(UserSchema.safeParse(user).success).toBe(true)
})
```

## Not testing at boundaries

```typescript
// BAD: schema unit tests pass but handler never calls safeParse
// tests/user-schema.test.ts
it("rejects invalid email", () => {
  expect(UserSchema.safeParse({ email: "bad" }).success).toBe(false)
})
// But the actual handler does:
app.post("/users", (req, res) => {
  // No validation! req.body goes straight to the database
  db.users.create(req.body)
})

// GOOD: integration test verifies the boundary actually validates
it("API rejects invalid input with field errors", async () => {
  const res = await request(app)
    .post("/api/users")
    .send({ name: "", email: "bad" })
    .expect(400)

  expect(res.body.errors.fieldErrors).toHaveProperty("email")
})

it("API accepts valid input", async () => {
  const res = await request(app)
    .post("/api/users")
    .send({ name: "Alice", email: "[email protected]", age: 30 })
    .expect(201)
})
```

## No snapshot regression testing

```typescript
// BAD: field removal goes unnoticed
// Someone removes `role` from UserSchema — no test catches it
const UserSchema = z.object({
  name: z.string(),
  email: z.email(),
  // role was here — removed without anyone noticing
})

// GOOD: JSON Schema snapshot catches schema changes
it("schema shape has not changed", () => {
  const jsonSchema = z.toJSONSchema(UserSchema)
  expect(jsonSchema).toMatchSnapshot()
  // If `role` is removed, snapshot diff shows it clearly in code review
})
```

## Testing schema shape but not error observability

```typescript
// BAD: tests verify schema accepts/rejects but never check error structure
it("rejects invalid input", () => {
  const result = UserSchema.safeParse({ email: "bad" })
  expect(result.success).toBe(false)
  // Never checks what the error looks like — logging could be broken
})

// GOOD: verify flattenError produces queryable structure
it("produces structured errors for logging", () => {
  const result = UserSchema.safeParse({ email: "bad" })
  expect(result.success).toBe(false)
  if (!result.success) {
    const flat = z.flattenError(result.error)
    expect(flat.fieldErrors).toHaveProperty("email")
    expect(flat.fieldErrors.email![0]).toBeTruthy()
    // Verifies that your logging pipeline will receive useful data
  }
})
```

## No drift detection workflow

```typescript
// BAD: schema changes land without mechanical review
// Developer changes OrderSchema, no CI check, no snapshot diff
// Consumers break silently because the field they depend on is gone

// GOOD: export and diff JSON Schema snapshots in CI
// scripts/export-schemas.ts
import { z } from "zod"
import { writeFileSync } from "fs"

const jsonSchema = z.toJSONSchema(OrderSchema)
writeFileSync(
  "snapshots/Order.json",
  JSON.stringify(jsonSchema, null, 2) + "\n"
)

// CI workflow:
// 1. Run export-schemas.ts
// 2. git diff snapshots/ — if changed, CI fails
// 3. Developer reviews diff, updates snapshots intentionally
// 4. Schema changes are always visible in code review
```

```

### references/api-reference.md

```markdown
# Zod Testing API Reference

## Schema Correctness Testing

### safeParse() for Tests

Always use `safeParse()` in tests. `parse()` throws, which crashes the test instead of producing a clear failure.

```typescript
// GOOD: assertion-based
const result = schema.safeParse(data)
expect(result.success).toBe(true)
// or
expect(result.success).toBe(false)

// BAD: crash-based — if schema changes, test behavior is unpredictable
expect(() => schema.parse(data)).toThrow()
```

### Testing Valid Data

```typescript
describe("UserSchema", () => {
  const validUsers = [
    { name: "Alice", email: "[email protected]", age: 30 },
    { name: "B", email: "[email protected]", age: 0 },
    { name: "Zed", email: "[email protected]", age: 150 },
  ]

  it.each(validUsers)("accepts valid user: $name", (user) => {
    const result = UserSchema.safeParse(user)
    expect(result.success).toBe(true)
    if (result.success) {
      expect(result.data).toEqual(user)
    }
  })
})
```

### Testing Invalid Data

```typescript
const invalidInputs = [
  { input: {}, desc: "empty object" },
  { input: { name: "" }, desc: "empty name" },
  { input: { name: "A", email: "bad" }, desc: "invalid email" },
  { input: { name: "A", email: "[email protected]", age: -1 }, desc: "negative age" },
  { input: { name: "A", email: "[email protected]", age: 151 }, desc: "age over max" },
]

it.each(invalidInputs)("rejects $desc", ({ input }) => {
  expect(UserSchema.safeParse(input).success).toBe(false)
})
```

### Boundary Value Testing

```typescript
const NumberSchema = z.number().min(0).max(100)

const boundaries = [
  { value: 0, expected: true, desc: "minimum" },
  { value: 100, expected: true, desc: "maximum" },
  { value: -1, expected: false, desc: "below minimum" },
  { value: 101, expected: false, desc: "above maximum" },
  { value: -0.001, expected: false, desc: "fractionally below min" },
  { value: 100.001, expected: false, desc: "fractionally above max" },
]

it.each(boundaries)("$desc ($value) → $expected", ({ value, expected }) => {
  expect(NumberSchema.safeParse(value).success).toBe(expected)
})
```

### Optional and Nullable Testing

```typescript
const Schema = z.object({
  required: z.string(),
  optional: z.string().optional(),
  nullable: z.string().nullable(),
  both: z.string().optional().nullable(),
})

it("accepts all optional/nullable combinations", () => {
  expect(Schema.safeParse({ required: "a" }).success).toBe(true)
  expect(Schema.safeParse({ required: "a", optional: undefined }).success).toBe(true)
  expect(Schema.safeParse({ required: "a", nullable: null }).success).toBe(true)
  expect(Schema.safeParse({ required: "a", both: null }).success).toBe(true)
  expect(Schema.safeParse({ required: "a", both: undefined }).success).toBe(true)
})

it("rejects null for non-nullable", () => {
  expect(Schema.safeParse({ required: "a", optional: null }).success).toBe(false)
})

it("rejects undefined for non-optional", () => {
  expect(Schema.safeParse({ required: undefined }).success).toBe(false)
})
```

## Error Assertion Patterns

### Using z.flattenError()

```typescript
it("reports field-level errors", () => {
  const result = UserSchema.safeParse({ name: "", email: "bad", age: -1 })
  expect(result.success).toBe(false)
  if (!result.success) {
    const flat = z.flattenError(result.error)
    expect(flat.fieldErrors).toHaveProperty("email")
    expect(flat.fieldErrors).toHaveProperty("age")
  }
})
```

### Asserting Error Messages

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

### Asserting Error Codes

```typescript
it("produces invalid_type for wrong type", () => {
  const result = z.number().safeParse("string")
  expect(result.success).toBe(false)
  if (!result.success) {
    expect(result.error.issues[0].code).toBe("invalid_type")
  }
})

it("produces too_small for under minimum", () => {
  const result = z.number().min(5).safeParse(3)
  expect(result.success).toBe(false)
  if (!result.success) {
    expect(result.error.issues[0].code).toBe("too_small")
  }
})
```

### Asserting Error Paths

```typescript
it("targets the correct field", () => {
  const result = FormSchema.safeParse({
    password: "12345678",
    confirm: "different",
  })
  expect(result.success).toBe(false)
  if (!result.success) {
    const confirmError = result.error.issues.find(
      (i) => i.path.join(".") === "confirm"
    )
    expect(confirmError).toBeDefined()
    expect(confirmError!.message).toBe("Passwords don't match")
  }
})
```

### Counting Errors

```typescript
it("reports all validation errors at once", () => {
  const result = UserSchema.safeParse({})
  expect(result.success).toBe(false)
  if (!result.success) {
    // Zod reports all issues, not just the first
    expect(result.error.issues.length).toBeGreaterThanOrEqual(3)
  }
})
```

## Mock Data Generation

### Using zod-schema-faker

```bash
npm install -D zod-schema-faker
```

```typescript
import { install, fake, seed } from "zod-schema-faker"
import { z } from "zod"

// Call once in test setup (beforeAll or setupFiles)
install(z)

const UserSchema = z.object({
  name: z.string().min(1).max(50),
  email: z.email(),
  age: z.number().int().min(0).max(150),
  role: z.enum(["admin", "user", "guest"]),
})

describe("with generated data", () => {
  beforeEach(() => seed(42)) // deterministic

  it("schema accepts its own generated data", () => {
    const user = fake(UserSchema)
    expect(UserSchema.safeParse(user).success).toBe(true)
  })

  it("generates array of valid items", () => {
    const users = Array.from({ length: 10 }, () => fake(UserSchema))
    users.forEach((u) => {
      expect(UserSchema.safeParse(u).success).toBe(true)
    })
  })
})
```

### Using @anatine/zod-mock

```bash
npm install -D @anatine/zod-mock @faker-js/faker
```

```typescript
import { generateMock } from "@anatine/zod-mock"

it("generates valid mock", () => {
  const mock = generateMock(UserSchema)
  expect(UserSchema.safeParse(mock).success).toBe(true)
})

// With seed for deterministic output
it("generates consistent mock", () => {
  const mock = generateMock(UserSchema, { seed: 123 })
  expect(mock.name).toBeDefined()
})
```

## Snapshot Testing

### JSON Schema Snapshots

```typescript
import { z } from "zod"

it("schema shape matches snapshot", () => {
  const jsonSchema = z.toJSONSchema(UserSchema)
  expect(jsonSchema).toMatchSnapshot()
})
```

Update snapshots when schema changes are intentional:
```bash
vitest --update  # or jest --updateSnapshot
```

### Formatted Error Snapshots

```typescript
it("error output matches snapshot", () => {
  const result = UserSchema.safeParse({ name: 123, email: "bad" })
  expect(result.success).toBe(false)
  if (!result.success) {
    const flat = z.flattenError(result.error)
    expect(flat).toMatchSnapshot()
  }
})
```

## Integration Testing

### Express/Fastify API Handler

```typescript
import request from "supertest"

it("validates request body", async () => {
  const res = await request(app)
    .post("/api/users")
    .send({ name: "", email: "invalid" })
    .expect(400)

  expect(res.body.errors.fieldErrors).toHaveProperty("email")
})

it("accepts valid request", async () => {
  const res = await request(app)
    .post("/api/users")
    .send({ name: "Alice", email: "[email protected]", age: 30 })
    .expect(201)

  expect(res.body.id).toBeDefined()
})
```

### Form Validation

```typescript
it("produces form-compatible errors", () => {
  const result = FormSchema.safeParse(formData)
  if (!result.success) {
    const errors = z.flattenError(result.error)
    // Structure matches what form libraries expect
    expect(errors).toEqual({
      formErrors: expect.any(Array),
      fieldErrors: expect.objectContaining({
        email: expect.any(Array),
      }),
    })
  }
})
```

### Database Layer

```typescript
it("validates before insert", async () => {
  const invalidUser = { name: "", email: "bad" }
  const result = UserSchema.safeParse(invalidUser)
  expect(result.success).toBe(false)
  // Verify no database call was made
  expect(db.insert).not.toHaveBeenCalled()
})
```

## Property-Based Testing

### With fast-check

```bash
npm install -D fast-check
```

```typescript
import fc from "fast-check"
import { fake, seed } from "zod-schema-faker"

it("schema always accepts its own generated data", () => {
  fc.assert(
    fc.property(fc.integer({ min: 0, max: 10000 }), (s) => {
      seed(s)
      const data = fake(UserSchema)
      return UserSchema.safeParse(data).success
    }),
    { numRuns: 200 }
  )
})
```

### Transform Round-Trip Testing

```typescript
it("transform preserves data integrity", () => {
  fc.assert(
    fc.property(fc.integer({ min: 0, max: 10000 }), (s) => {
      seed(s)
      const input = fake(InputSchema)
      const result = InputSchema.safeParse(input)
      if (result.success) {
        // Verify output type matches expected shape
        expect(OutputSchema.safeParse(result.data).success).toBe(true)
      }
      return true
    }),
  )
})
```

## Async Schema Testing

```typescript
it("validates async refinements", async () => {
  const UniqueEmail = z.email().refine(
    async (email) => !(await db.exists(email)),
    { error: "Already registered" }
  )

  // Must use safeParseAsync
  const result = await UniqueEmail.safeParseAsync("[email protected]")
  expect(result.success).toBe(false)
})
```

## Test Helpers

### reusable assertion helper

```typescript
function expectValid<T>(schema: z.ZodType<T>, data: unknown) {
  const result = schema.safeParse(data)
  if (!result.success) {
    throw new Error(
      `Expected valid but got errors:\n${z.prettifyError(result.error)}`
    )
  }
  return result.data
}

function expectInvalid(schema: z.ZodType, data: unknown) {
  const result = schema.safeParse(data)
  expect(result.success).toBe(false)
  if (!result.success) return result.error
  throw new Error("Expected invalid but data was accepted")
}

// Usage
it("accepts valid user", () => {
  const user = expectValid(UserSchema, validData)
  expect(user.name).toBe("Alice")
})

it("rejects invalid email", () => {
  const error = expectInvalid(UserSchema, { ...validData, email: "bad" })
  expect(error.issues[0].path).toEqual(["email"])
})
```

## Structural Testing

### Schema Dependency Direction

Assert that schemas are imported from boundary layers, not from domain code. This enforces the architectural rule that parsing happens at boundaries.

```typescript
import { execSync } from "child_process"

describe("schema architecture", () => {
  it("domain layer does not import raw schemas", () => {
    // grep for Zod schema imports in domain code
    const result = execSync(
      'grep -r "from.*schemas" src/domain/ || true',
      { encoding: "utf-8" }
    )
    // Domain should only import types (z.infer), not raw schemas
    const rawImports = result
      .split("\n")
      .filter((line) => line && !line.includes("type {") && !line.includes("type{"))
    expect(rawImports).toHaveLength(0)
  })
})
```

### Circular Import Detection

Use madge to detect circular dependencies between schema files:

```typescript
import madge from "madge"

describe("schema dependencies", () => {
  it("has no circular dependencies", async () => {
    const result = await madge("src/", {
      fileExtensions: ["ts"],
      tsConfigPath: "tsconfig.json",
    })
    const circular = result.circular()
    expect(circular).toHaveLength(0)
  })
})
```

## Drift Detection

### JSON Schema Snapshot Workflow

Export schemas as JSON Schema and commit snapshots. CI fails when schemas change without updating snapshots.

```typescript
// scripts/export-schemas.ts
import { z } from "zod"
import { writeFileSync, mkdirSync } from "fs"
import { UserSchema, OrderSchema } from "../src/api/schemas"

const schemas = { User: UserSchema, Order: OrderSchema }

mkdirSync("snapshots", { recursive: true })
for (const [name, schema] of Object.entries(schemas)) {
  writeFileSync(
    `snapshots/${name}.json`,
    JSON.stringify(z.toJSONSchema(schema), null, 2) + "\n"
  )
}
```

### Snapshot Test

```typescript
import { readFileSync } from "fs"

describe("schema drift detection", () => {
  it("UserSchema matches committed snapshot", () => {
    const current = z.toJSONSchema(UserSchema)
    const committed = JSON.parse(
      readFileSync("snapshots/User.json", "utf-8")
    )
    expect(current).toEqual(committed)
  })
})
```

### CI Integration

```yaml
# .github/workflows/schema-check.yml
- run: npx tsx scripts/export-schemas.ts
- name: Check for schema drift
  run: |
    if git diff --exit-code snapshots/; then
      echo "Schemas unchanged"
    else
      echo "::error::Schema snapshots changed. Update snapshots if intentional."
      git diff snapshots/
      exit 1
    fi
```

## Observable Error Testing

### Assert flattenError Structure

Test that validation errors produce the expected structured output for logging and monitoring.

```typescript
describe("error observability", () => {
  it("flattenError has expected field keys", () => {
    const result = UserSchema.safeParse({ name: "", email: "bad" })
    expect(result.success).toBe(false)
    if (!result.success) {
      const flat = z.flattenError(result.error)
      // Verify structure matches what logging expects
      expect(flat).toHaveProperty("formErrors")
      expect(flat).toHaveProperty("fieldErrors")
      expect(flat.fieldErrors).toHaveProperty("email")
    }
  })

  it("error messages are user-facing quality", () => {
    const Schema = z.object({
      email: z.email({ error: "Please enter a valid email" }),
      age: z.number({ error: "Age must be a number" }).min(0, "Age cannot be negative"),
    })

    const result = Schema.safeParse({ email: "bad", age: -1 })
    expect(result.success).toBe(false)
    if (!result.success) {
      const flat = z.flattenError(result.error)
      expect(flat.fieldErrors.email![0]).toBe("Please enter a valid email")
      expect(flat.fieldErrors.age![0]).toBe("Age cannot be negative")
    }
  })
})
```

## Performance Testing

### Benchmark Parse Time

```typescript
describe("schema performance", () => {
  it("UserSchema parses within acceptable time", () => {
    const validUser = { name: "Alice", email: "[email protected]", age: 30 }

    const start = performance.now()
    for (let i = 0; i < 10_000; i++) {
      UserSchema.safeParse(validUser)
    }
    const elapsed = performance.now() - start

    // 10k parses should complete in under 100ms
    expect(elapsed).toBeLessThan(100)
  })
})
```

### With Vitest Bench

```typescript
import { bench, describe } from "vitest"

describe("schema benchmarks", () => {
  const validUser = { name: "Alice", email: "[email protected]", age: 30 }

  bench("UserSchema.safeParse", () => {
    UserSchema.safeParse(validUser)
  })

  bench("UserSchema.safeParse (invalid)", () => {
    UserSchema.safeParse({ name: 123 })
  })
})
```

Run with:
```bash
vitest bench
```

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
# Zod Testing

Created by **[Anivar Aravind](https://anivar.net)**

An AI agent skill for testing Zod schemas with Jest and Vitest.

## The Problem

AI agents often write schema tests that only check the happy path, use `parse()` instead of `safeParse()` (crashing instead of failing), test schema internals (`.shape`, `._def`) instead of behavior, or hardcode mock data instead of generating it from the schema. The result: tests that miss regressions and break on harmless refactors.

## This Solution

A focused testing skill covering schema correctness testing, error assertion patterns, mock data generation, snapshot testing with `z.toJSONSchema()`, property-based testing, structural testing, and drift detection — with 14 anti-patterns showing exactly what goes wrong and how to fix it.

## Install

```bash
npx skills add anivar/zod-testing -g
```

Or with full URL:

```bash
npx skills add https://github.com/anivar/zod-testing
```

## Baseline

- zod ^4.0.0
- Jest or Vitest
- TypeScript ^5.5

## What's Inside

### Testing Approaches

| Approach | Type | Use When |
|----------|------|----------|
| `safeParse()` result checking | Correctness | Default — always use safeParse in tests |
| `z.flattenError()` assertions | Error messages | Verifying specific field errors |
| `z.toJSONSchema()` snapshots | Schema shape | Detecting unintended schema changes |
| Mock data generation | Fixtures | Need valid/randomized test data |
| Property-based testing | Fuzz testing | Schemas must handle arbitrary valid inputs |
| Structural testing | Architecture | Verify schemas are only imported at boundaries |
| Drift detection | Regression | Catch unintended schema changes via JSON Schema snapshots |

### Anti-Patterns

14 common testing mistakes with BAD/GOOD code examples:
- Testing schema internals instead of behavior
- Not testing error paths (only happy path)
- Using `parse()` in tests (crashes instead of failing)
- Not testing boundary values (min/max edges)
- Hardcoding mock data instead of generating from schema
- Snapshot testing raw ZodError instead of formatted output
- Not seeding random data generators (flaky tests)
- Not testing at boundaries (schema tests pass but handler doesn't validate)
- No snapshot regression testing (field removal goes unnoticed)
- Testing schema shape but not error observability
- No drift detection workflow (schema changes land without review)

## Structure

```
├── SKILL.md                      # Entry point for AI agents
└── references/
    ├── api-reference.md          # Testing patterns, assertion helpers, mock generation
    └── anti-patterns.md          # Common testing mistakes to avoid
```

## Ecosystem — Skills by [Anivar Aravind](https://anivar.net)

### Testing Skills
| Skill | What it covers | Install |
|-------|---------------|---------|
| [jest-skill](https://github.com/anivar/jest-skill) | Jest best practices — mock design, async testing, matchers, timers, snapshots | `npx skills add anivar/jest-skill -g` |
| [msw-skill](https://github.com/anivar/msw-skill) | MSW 2.0 API mocking — handlers, responses, GraphQL | `npx skills add anivar/msw-skill -g` |
| [redux-saga-testing](https://github.com/anivar/redux-saga-testing) | Redux-Saga testing — expectSaga, testSaga, providers | `npx skills add anivar/redux-saga-testing -g` |

### Library & Framework Skills
| Skill | What it covers | Install |
|-------|---------------|---------|
| [zod-skill](https://github.com/anivar/zod-skill) | Zod v4 schema validation, parsing, error handling | `npx skills add anivar/zod-skill -g` |
| [redux-saga-skill](https://github.com/anivar/redux-saga-skill) | Redux-Saga effects, fork model, channels, RTK | `npx skills add anivar/redux-saga-skill -g` |

### Engineering Analysis
| Skill | What it covers | Install |
|-------|---------------|---------|
| [contributor-codebase-analyzer](https://github.com/anivar/contributor-codebase-analyzer) | Code analysis, annual reviews, promotion readiness | `npx skills add anivar/contributor-codebase-analyzer -g` |

## Author

**[Anivar Aravind](https://anivar.net)** — Building AI agent skills for modern JavaScript/TypeScript development.

## License

MIT

```

### _meta.json

```json
{
  "owner": "anivar",
  "slug": "zod-testing",
  "displayName": "Zod Testing",
  "latest": {
    "version": "1.0.1",
    "publishedAt": 1772684719278,
    "commit": "https://github.com/openclaw/skills/commit/bbfd874501caf0899ed785dcfe3c2bba1a40c637"
  },
  "history": []
}

```

zod-testing | SkillHub