При разработке приложений схемы валидации часто становятся частью бизнес-логики: проверяют API-запросы, валидируют конфигурации, описывают DTO, параметры окружения, формы и ответы внешних сервисов. В тестировании возникает необходимость:
В контексте Zod под моками и стабами обычно подразумеваются:
| Тип | Назначение |
|---|---|
| Mock | Генерация фальшивых данных |
| Stub | Упрощённая замена реальной схемы |
| Fixture | Заранее подготовленный набор данных |
| Fake validator | Искусственная схема с контролируемым поведением |
import { z } from "zod"
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number().min(18),
})
Для тестов понадобится множество валидных объектов:
{
id: 1,
name: "Alex",
email: "alex@test.com",
age: 25
}
Ручное создание быстро становится проблемой:
Наиболее популярная библиотека генерации моков —
@anatine/zod-mock.
npm install @anatine/zod-mock faker
import { z } from "zod"
import { generateMock } from "@anatine/zod-mock"
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
})
const mock = generateMock(UserSchema)
console.log(mock)
Пример результата:
{
id: 42,
name: "John Doe",
email: "test@example.com"
}
const AddressSchema = z.object({
city: z.string(),
country: z.string(),
})
const UserSchema = z.object({
id: z.number(),
profile: z.object({
firstName: z.string(),
lastName: z.string(),
}),
address: AddressSchema,
})
const mock = generateMock(UserSchema)
Результат:
{
id: 10,
profile: {
firstName: "Jane",
lastName: "Smith"
},
address: {
city: "Paris",
country: "France"
}
}
const PostSchema = z.object({
title: z.string(),
tags: z.array(z.string()),
})
const mock = generateMock(PostSchema)
Результат:
{
title: "Example",
tags: ["tag1", "tag2"]
}
const UserSchema = z.object({
id: z.number(),
name: z.string(),
})
const mock = generateMock(UserSchema, {
stringMap: {
name: () => "ADMIN_USER",
},
})
Результат:
{
id: 55,
name: "ADMIN_USER"
}
const RoleSchema = z.enum([
"admin",
"moderator",
"user",
])
const mock = generateMock(RoleSchema)
Результат:
"admin"
const UserSchema = z.object({
role: RoleSchema,
})
const mock = generateMock(UserSchema)
const ResponseSchema = z.union([
z.object({
status: z.literal("success"),
data: z.string(),
}),
z.object({
status: z.literal("error"),
message: z.string(),
}),
])
const mock = generateMock(ResponseSchema)
Библиотека выберет один из вариантов union.
const ShapeSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("circle"),
radius: z.number(),
}),
z.object({
type: z.literal("square"),
size: z.number(),
}),
])
const mock = generateMock(ShapeSchema)
const UserSchema = z.object({
name: z.string(),
nickname: z.string().optional(),
})
Мок может содержать:
{
name: "Alex"
}
или:
{
name: "Alex",
nickname: "Lex"
}
const Schema = z.object({
description: z.string().nullable(),
})
Возможные результаты:
{
description: null
}
или:
{
description: "Example"
}
import { faker } from "@faker-js/faker"
const user = {
id: faker.number.int(),
name: faker.person.fullName(),
email: faker.internet.email(),
}
После генерации данные можно проверить через Zod:
UserSchema.parse(user)
const createUserMock = () => {
const user = {
id: faker.number.int(),
name: faker.person.fullName(),
email: faker.internet.email(),
age: faker.number.int({
min: 18,
max: 70,
}),
}
return UserSchema.parse(user)
}
Преимущества:
Иногда настоящая схема слишком сложная:
const RealSchema = z.object({
database: z.object({
host: z.string(),
port: z.number(),
credentials: z.object({
login: z.string(),
password: z.string(),
}),
}),
services: z.array(
z.object({
name: z.string(),
url: z.string().url(),
}),
),
})
Для unit-теста может быть достаточно упрощённой версии:
const StubSchema = z.object({
database: z.any(),
services: z.any(),
})
import * as schemaModule from "./schema"
jest.spyOn(schemaModule.UserSchema, "parse")
.mockImplementation((data) => data)
Теперь parse не выполняет валидацию.
jest.spyOn(schemaModule.UserSchema, "parse")
.mockImplementation(() => {
throw new Error("Validation failed")
})
jest.spyOn(UserSchema, "safeParse")
.mockReturnValue({
success: true,
data: {
id: 1,
name: "Test",
},
})
jest.spyOn(UserSchema, "safeParse")
.mockReturnValue({
success: false,
error: new z.ZodError([]),
})
const PasswordSchema = z.string().refine(
(value) => value.length >= 8,
{
message: "Password too short",
},
)
const result = PasswordSchema.safeParse("123")
expect(result.success).toBe(false)
const RegisterSchema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Passwords mismatch",
path: ["confirmPassword"],
})
}
})
const result = RegisterSchema.safeParse({
password: "12345678",
confirmPassword: "11111111",
})
expect(result.success).toBe(false)
const createUser = (
overrides?: Partial<z.infer<typeof UserSchema>>,
) => {
return {
id: 1,
name: "John",
email: "john@test.com",
age: 30,
...overrides,
}
}
const createValidatedUser = (
overrides?: Partial<z.infer<typeof UserSchema>>,
) => {
return UserSchema.parse({
id: 1,
name: "John",
email: "john@test.com",
age: 30,
...overrides,
})
}
const PartialUserSchema = UserSchema.partial()
Теперь все поля необязательны.
const patchData = {
email: "new@test.com",
}
PartialUserSchema.parse(patchData)
Для вложенных структур:
const SettingsSchema = z.object({
ui: z.object({
theme: z.string(),
language: z.string(),
}),
})
const PartialSchema = SettingsSchema.deepPartial()
Теперь необязательными становятся все вложенные поля.
const ApiResponseSchema = z.object({
success: z.boolean(),
data: z.array(
z.object({
id: z.number(),
title: z.string(),
}),
),
})
const responseMock = {
success: true,
data: [
{
id: 1,
title: "Post",
},
],
}
ApiResponseSchema.parse(responseMock)
const invalidUser = {
id: "wrong",
}
const result = UserSchema.safeParse(invalidUser)
expect(result.success).toBe(false)
const invalidUser = {
id: 1,
}
const result = UserSchema.safeParse({
id: "abc",
})
if (!result.success) {
console.log(result.error.issues)
}
Пример:
[
{
code: "invalid_type",
expected: "number",
received: "string",
path: ["id"],
message: "Expected number"
}
]
expect(
UserSchema.safeParse({
id: "wrong",
}),
).toMatchSnapshot()
Модуль:
export const createUser = (input: unknown) => {
const data = UserSchema.parse(input)
return database.save(data)
}
Unit-тест функции не обязан проверять корректность схемы.
jest.spyOn(UserSchema, "parse")
.mockImplementation((data) => ({
id: 1,
name: "Test",
email: "test@test.com",
age: 30,
}))
Теперь тестируется только логика createUser.
const EdgeCaseSchema = z.object({
min: z.number().min(0),
max: z.number().max(100),
})
Пограничные значения:
[
{ min: 0, max: 100 },
{ min: 1, max: 99 },
]
npm install fast-check
import fc from "fast-check"
fc.assert(
fc.property(
fc.string(),
(value) => {
const result = z.string().safeParse(value)
return result.success
},
),
)
Каждый мок желательно пропускать через parse:
const mock = createUser()
UserSchema.parse(mock)
Это предотвращает:
type User = z.infer<typeof UserSchema>
const createUser = (
overrides?: Partial<User>,
): User => ({
id: 1,
name: "Alex",
email: "alex@test.com",
age: 25,
...overrides,
})
export const users = {
admin: {
id: 1,
role: "admin",
},
guest: {
id: 2,
role: "guest",
},
}
Изменение схемы:
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
})
требует обновления:
Автоматическая проверка через parse помогает выявлять
такие изменения мгновенно.
src/
tests/
fixtures/
factories/
mocks/
stubs/
| Категория | Назначение |
|---|---|
| fixtures | Статические данные |
| factories | Генерация объектов |
| mocks | Имитация поведения |
| stubs | Подмена зависимостей |
Плохо:
const user = {
id: "1",
}
если схема ожидает number.
Плохо:
expect(user).toEqual({
id: expect.any(Number),
name: expect.any(String),
})
Лучше:
UserSchema.parse(user)
Недостаточно проверять только валидные данные.
Необходимо тестировать:
При schema-first подходе Zod становится центральным источником правды:
Это устраняет рассинхронизацию между: