zod-testing について
zod-testingは、Zodスキーマをテストする開発者向けガイドです。バリデーション、エラー検証、テストデータ生成、スナップショット、統合テストに加え、Zod 4の挙動も扱います。
主な機能
- 01有効・無効な入力のテスト
- 02エラーとフィールド別の問題の検証
- 03境界値と任意値のテスト
- 04シード付きモックデータの生成
- 05JSON Schema出力のスナップショット
- 06APIとフォームの統合テスト
活用シーン
- 01入力の受理・拒否の検証
- 02意図しないスキーマ変更の検出
- 03API境界でのバリデーションのテスト
- 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 ハンドラー、フォーム、データベース層と正しく連携するか?
- エッジケース — 境界値、オプション/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 }
)
})
ルール
- テストでは必ず
safeParse()を使用する —parse()はテストを失敗させるのではなく、クラッシュさせます - 有効なデータと無効なデータの両方をテストする — 成功するケースだけをテストしないでください
- 境界値をテストする — 数値制約では min、max、min-1、max+1 をテストします
- オプション/nullable の組み合わせをテストする — undefined、null、キーの欠落
- 具体的なエラーフィールドをアサートする —
z.flattenError()を使って、どのフィールドで失敗したかを確認します - スキーマの内部実装をテストしない —
.shapeや._defではなく、パース結果をテストします z.toJSONSchema()のスナップショットを使用する — 意図しないスキーマ変更を検出します- 乱数ジェネレーターにシードを設定する — 非決定的なテストは不安定なテストです
- 変換を個別にテストする — 入力のバリデーションと出力の変換の両方を検証します
- アサーションでスキーマのロジックを重複させない — 実装ではなく動作をテストします
アンチパターン
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())には影響ありません。エラーが直ちに生成されます。
参考資料
- API リファレンス — テストのパターン、アサーションヘルパー、モックの生成
- アンチパターン — 避けるべき一般的なテストの間違い