Библиотека Zod предназначена не только для валидации входящих данных, но и для построения полноценной инфраструктуры вокруг схем: генерации типов, трансформаций, безопасного парсинга, мокирования и автоматического создания тестовых наборов. Генерация тестовых данных особенно полезна при:
Сам Zod не содержит встроенного генератора случайных данных, однако его схемы идеально подходят как источник метаданных для внешних библиотек.
Любая схема Zod описывает структуру данных:
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number().min(18),
});
На основе этой схемы можно:
Наиболее популярные решения:
| Библиотека | Назначение |
|---|---|
@faker-js/faker |
Генерация случайных данных |
zod-fixture |
Создание объектов из схем Zod |
zod-mock |
Автоматическая генерация mock-данных |
fast-check |
Property-based testing |
@anatine/zod-mock |
Продвинутое мокирование |
chance |
Генератор случайных значений |
npm install zod @faker-js/faker
Самый простой подход — комбинировать Zod и Faker.
import { z } from "zod";
import { faker } from "@faker-js/faker";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const mockUser = {
id: faker.number.int(),
name: faker.person.fullName(),
email: faker.internet.email(),
};
UserSchema.parse(mockUser);
console.log(mockUser);
Результат:
{
id: 4832,
name: "John Smith",
email: "john@example.com"
}
Даже при использовании Faker данные могут нарушать ограничения схемы.
Пример:
const UserSchema = z.object({
age: z.number().min(18),
});
Неверная генерация:
const user = {
age: faker.number.int({ min: 1, max: 10 }),
};
UserSchema.parse(user);
Ошибка:
ZodError: Number must be greater than or equal to 18
Правильный вариант:
const user = {
age: faker.number.int({ min: 18, max: 90 }),
};
npm install @anatine/zod-mock
import { z } from "zod";
import { generateMock } from "@anatine/zod-mock";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
active: z.boolean(),
});
const mock = generateMock(UserSchema);
console.log(mock);
Результат:
{
id: 123,
name: "string",
active: true
}
const AddressSchema = z.object({
city: z.string(),
street: z.string(),
});
const UserSchema = z.object({
id: z.number(),
address: AddressSchema,
});
const mock = generateMock(UserSchema);
Результат:
{
id: 123,
address: {
city: "string",
street: "string"
}
}
const PostSchema = z.object({
title: z.string(),
tags: z.array(z.string()),
});
const mock = generateMock(PostSchema);
Результат:
{
title: "string",
tags: ["string"]
}
const ResultSchema = z.union([
z.string(),
z.number(),
]);
const mock = generateMock(ResultSchema);
Библиотека выбирает один из вариантов:
"string"
или
123
const RoleSchema = z.enum([
"admin",
"user",
"moderator",
]);
const mock = generateMock(RoleSchema);
Результат:
"admin"
const UserSchema = z.object({
name: z.string(),
bio: z.string().optional(),
avatar: z.string().nullable(),
});
Генерация:
const mock = generateMock(UserSchema);
Возможный результат:
{
name: "string",
bio: "string",
avatar: null
}
Некоторые ограничения схемы не учитываются автоматически.
Пример:
const PasswordSchema = z.string().min(20);
Многие mock-генераторы создадут:
"string"
что не пройдет валидацию.
Поэтому после генерации рекомендуется выполнять дополнительную проверку:
const mock = generateMock(PasswordSchema);
PasswordSchema.parse(mock);
Большинство библиотек поддерживают переопределение генераторов.
Пример:
import { generateMock } from "@anatine/zod-mock";
const UserSchema = z.object({
email: z.string().email(),
});
const mock = generateMock(UserSchema, {
stringMap: {
email: () => "admin@example.com",
},
});
Схемы Zod могут содержать трансформации.
const UserSchema = z.object({
name: z.string(),
}).transform(data => ({
...data,
slug: data.name.toLowerCase(),
}));
Важно понимать различие:
Пример:
const input = {
name: "John",
};
const result = UserSchema.parse(input);
console.log(result);
Результат:
{
name: "John",
slug: "john"
}
Схемы часто используются как контракты API.
const CreateUserRequestSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
Генератор:
const requestBody = {
email: faker.internet.email(),
password: faker.internet.password({
length: 12,
}),
};
CreateUserRequestSchema.parse(requestBody);
const users = Array.from({ length: 100 }, () => ({
id: faker.number.int(),
name: faker.person.fullName(),
email: faker.internet.email(),
}));
Проверка:
users.forEach(user => {
UserSchema.parse(user);
});
Генерация сидов для базы данных:
const seedUsers = Array.from({ length: 10 }, () => ({
email: faker.internet.email(),
name: faker.person.fullName(),
age: faker.number.int({
min: 18,
max: 80,
}),
}));
Для повторяемых тестов необходимо фиксировать seed.
faker.seed(123);
Теперь генерация станет предсказуемой:
console.log(faker.person.fullName());
Каждый запуск вернет одинаковое значение.
Property-based testing предполагает генерацию большого количества случайных входных данных.
Для этого часто используется fast-check.
npm install fast-check
import fc from "fast-check";
fc.assert(
fc.property(fc.integer(), value => {
return value + 1 > value;
})
);
const UserSchema = z.object({
id: z.number(),
name: z.string(),
});
Тест:
fc.assert(
fc.property(
fc.record({
id: fc.integer(),
name: fc.string(),
}),
data => {
const result = UserSchema.safeParse(data);
return result.success;
}
)
);
Тестовые данные должны покрывать:
Пример:
const StringSchema = z.string().min(5);
const values = [
"",
"a",
"abc",
"hello",
"очень длинная строка",
];
Проверка:
values.forEach(value => {
console.log(
value,
StringSchema.safeParse(value).success
);
});
Тестирование ошибок не менее важно, чем успешных сценариев.
const UserSchema = z.object({
email: z.string().email(),
});
Набор невалидных данных:
const invalidEmails = [
"",
"test",
"test@",
"@gmail.com",
"invalid-email",
];
Проверка:
invalidEmails.forEach(email => {
const result = UserSchema.safeParse({ email });
console.log(result.success);
});
Метод refine часто требует отдельной генерации
данных.
const PasswordSchema = z.string().refine(
value => /[A-Z]/.test(value),
{
message: "Password must contain uppercase letter",
}
);
Тестовые данные:
const passwords = [
"password",
"PASSWORD",
"Pass123",
];
const EventSchema = z.object({
createdAt: z.date(),
});
Генерация:
const event = {
createdAt: faker.date.recent(),
};
const UserSchema = z.object({
id: z.string().uuid(),
});
Mock:
const user = {
id: faker.string.uuid(),
};
const SiteSchema = z.object({
url: z.string().url(),
});
const site = {
url: faker.internet.url(),
};
const ShapeSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("circle"),
radius: z.number(),
}),
z.object({
type: z.literal("square"),
size: z.number(),
}),
]);
Генерация:
const shape = generateMock(ShapeSchema);
Результат:
{
type: "circle",
radius: 100
}
Рекурсивные структуры:
const CategorySchema: z.ZodType<any> = z.object({
name: z.string(),
children: z.array(
z.lazy(() => CategorySchema)
),
});
Такие схемы сложнее для генерации.
Некоторые библиотеки:
Пример ручной генерации:
function createCategory(depth = 0): any {
if (depth > 3) {
return {
name: "Leaf",
children: [],
};
}
return {
name: faker.commerce.department(),
children: [
createCategory(depth + 1),
],
};
}
import { describe, it, expect } from "vitest";
describe("User validation", () => {
it("should validate generated user", () => {
const user = {
id: faker.number.int(),
email: faker.internet.email(),
};
const result = UserSchema.safeParse(user);
expect(result.success).toBe(true);
});
});
describe("User schema", () => {
test("generated user is valid", () => {
const user = {
id: faker.number.int(),
name: faker.person.fullName(),
};
expect(() => {
UserSchema.parse(user);
}).not.toThrow();
});
});
test("user snapshot", () => {
faker.seed(1);
const user = {
id: faker.number.int(),
name: faker.person.fullName(),
};
expect(user).toMatchSnapshot();
});
Playwright:
const user = {
email: faker.internet.email(),
password: faker.internet.password(),
};
Cypress:
cy.request("POST", "/users", user);
Плюсы:
Минусы:
Плюсы:
Минусы:
Наиболее распространенная стратегия:
При генерации больших объемов данных могут возникать проблемы:
Пример оптимизации:
const ParsedUserSchema = UserSchema.strict();
Строгие схемы быстрее выявляют ошибки структуры.
Полезная практика — тестировать:
Практика крупных проектов — использование фабрик.
function createUser(overrides = {}) {
return {
id: faker.number.int(),
name: faker.person.fullName(),
email: faker.internet.email(),
...overrides,
};
}
Использование:
const admin = createUser({
role: "admin",
});
const user = createUser();
UserSchema.parse(user);
Фабрика автоматически становится self-validating.
type User = z.infer<typeof UserSchema>;
function createUser(): User {
return {
id: faker.number.int(),
name: faker.person.fullName(),
email: faker.internet.email(),
};
}
Zod-схемы часто используются как единый источник истины между frontend и backend.
const ApiResponseSchema = z.object({
success: z.boolean(),
data: z.array(UserSchema),
});
Генерация ответа:
const response = {
success: true,
data: Array.from({ length: 5 }, createUser),
};
Валидация:
ApiResponseSchema.parse(response);
z.string().min(100)
Mock-генератор может создать слишком короткую строку.
z.string().refine(...)
Большинство генераторов не умеют анализировать пользовательскую логику.
.transform()
Трансформации часто выполняются уже после генерации.
z.lazy()
Могут вызывать бесконечную генерацию.
Распространенная структура проекта:
src/
schemas/
factories/
mocks/
tests/
Пример:
factories/
user.factory.ts
post.factory.ts
comment.factory.ts
import { faker } from "@faker-js/faker";
export function createPost(overrides = {}) {
return {
id: faker.number.int(),
title: faker.lorem.sentence(),
content: faker.lorem.paragraph(),
published: faker.datatype.boolean(),
...overrides,
};
}
Схема:
const PostSchema = z.object({
id: z.number(),
title: z.string(),
content: z.string(),
published: z.boolean(),
});
Тест:
const post = createPost();
PostSchema.parse(post);
function createComment(postId: number) {
return {
id: faker.number.int(),
postId,
text: faker.lorem.sentence(),
};
}
function createPostWithComments() {
const post = createPost();
return {
...post,
comments: Array.from(
{ length: 3 },
() => createComment(post.id)
),
};
}
const PostWithCommentsSchema = z.object({
id: z.number(),
title: z.string(),
comments: z.array(
z.object({
id: z.number(),
postId: z.number(),
text: z.string(),
})
),
});
Zod часто используется вместе с Prisma для генерации seed-данных.
await prisma.user.create({
data: createUser(),
});
Автоматическая генерация помогает:
Рекомендуемые правила:
z.infer.