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. 05JSON Schema出力のスナップショット
  6. 06APIとフォームの統合テスト

活用シーン

  1. 01入力の受理・拒否の検証
  2. 02意図しないスキーマ変更の検出
  3. 03API境界でのバリデーションのテスト
  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. エッジケース — 境界値、オプション/nullable の組み合わせ、空の入力

基本パターン

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. オプション/nullable の組み合わせをテストする — undefined、null、キーの欠落
  5. 具体的なエラーフィールドをアサートする — z.flattenError() を使って、どのフィールドで失敗したかを確認します
  6. スキーマの内部実装をテストしない — .shape や ._def ではなく、パース結果をテストします
  7. z.toJSONSchema() のスナップショットを使用する — 意図しないスキーマ変更を検出します
  8. 乱数ジェネレーターにシードを設定する — 非決定的なテストは不安定なテストです
  9. 変換を個別にテストする — 入力のバリデーションと出力の変換の両方を検証します
  10. アサーションでスキーマのロジックを重複させない — 実装ではなく動作をテストします

アンチパターン

references/anti-patterns.md に、以下の BAD/GOOD の例があります。

  • 動作ではなくスキーマの内部実装をテストする
  • エラー経路をテストしない
  • テストで 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() を使用してください。テストスイートでスキーマを頻繁に使う場合は、z.compile() と .validate() を組み合わせることで、アサーションのオーバーヘッドの大部分を取り除けます。

エラーマップは遅延評価されます。メッセージをアサーションする前に result.error を読み取ってください

4.6 以降では、エラーマップ(グローバル、ロケール、スキーマごとの 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())には影響ありません。エラーが直ちに生成されます。

参考資料

コメント