Схемы валидации становятся критически важной частью приложения, когда через них проходят:
Ошибка в схеме приводит не только к неверной проверке данных, но и к повреждению бизнес-логики, падениям приложения или скрытым багам.
Тестирование схем в Zod позволяет:
Главная идея тестирования схем — проверка двух сценариев:
Простейшая схема:
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(2),
age: z.number().int().positive(),
});
Тесты:
import { describe, expect, it } from "vitest";
describe("UserSchema", () => {
it("валидирует корректные данные", () => {
const data = {
name: "Alex",
age: 25,
};
const result = UserSchema.safeParse(data);
expect(result.success).toBe(true);
});
it("отклоняет некорректные данные", () => {
const data = {
name: "A",
age: -5,
};
const result = UserSchema.safeParse(data);
expect(result.success).toBe(false);
});
});
safeParse() удобнее в тестахМетод parse() выбрасывает исключение:
UserSchema.parse(data);
При ошибке:
ZodError
Для тестирования чаще используется safeParse():
const result = UserSchema.safeParse(data);
Результат:
{
success: true,
data
}
или:
{
success: false,
error
}
Это упрощает проверки и делает тесты более читаемыми.
successconst result = UserSchema.safeParse({
name: "John",
age: 30,
});
expect(result.success).toBe(true);
Особенно важно для схем с transform().
const schema = z.string().transform((v) => v.trim());
const result = schema.safeParse(" hello ");
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe("hello");
}
const result = UserSchema.safeParse({
name: "",
age: -10,
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues.length).toBe(2);
}
if (!result.success) {
expect(result.error.issues[0].path).toEqual(["name"]);
expect(result.error.issues[0].message).toBe(
"String must contain at least 2 character(s)"
);
}
Каждая ошибка содержит объект:
{
code,
path,
message
}
Пример:
if (!result.success) {
expect(result.error.issues).toEqual([
expect.objectContaining({
path: ["email"],
code: "invalid_string",
}),
]);
}
flatten()Метод flatten() преобразует ошибки в удобный формат.
Схема:
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
Тест:
const result = schema.safeParse({
email: "wrong",
password: "123",
});
if (!result.success) {
const errors = result.error.flatten();
expect(errors.fieldErrors.email).toContain(
"Invalid email"
);
expect(errors.fieldErrors.password).toContain(
"String must contain at least 8 character(s)"
);
}
format()if (!result.success) {
const formatted = result.error.format();
expect(formatted.email?._errors).toContain(
"Invalid email"
);
}
const AddressSchema = z.object({
city: z.string(),
zip: z.string().length(6),
});
const UserSchema = z.object({
name: z.string(),
address: AddressSchema,
});
Тест:
const result = UserSchema.safeParse({
name: "Alex",
address: {
city: "Moscow",
zip: "123",
},
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual([
"address",
"zip",
]);
}
const schema = z.array(
z.string().min(3)
);
Тест:
const result = schema.safeParse([
"ok",
"hello",
]);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual([0]);
}
const schema = z.union([
z.string(),
z.number(),
]);
Корректные значения:
expect(schema.safeParse("hello").success)
.toBe(true);
expect(schema.safeParse(100).success)
.toBe(true);
Некорректные:
expect(schema.safeParse(true).success)
.toBe(false);
const schema = z.discriminatedUnion("type", [
z.object({
type: z.literal("user"),
name: z.string(),
}),
z.object({
type: z.literal("admin"),
permissions: z.array(z.string()),
}),
]);
Тест:
const result = schema.safeParse({
type: "admin",
permissions: [],
});
expect(result.success).toBe(true);
Проверка ошибок:
const result = schema.safeParse({
type: "admin",
});
expect(result.success).toBe(false);
const schema = z.string().refine(
(v) => v.startsWith("A"),
{
message: "Must start with A",
}
);
Тест:
expect(
schema.safeParse("Alex").success
).toBe(true);
expect(
schema.safeParse("John").success
).toBe(false);
const result = schema.safeParse("John");
if (!result.success) {
expect(result.error.issues[0].message)
.toBe("Must start with A");
}
superRefine() позволяет создавать несколько ошибок.
Схема:
const schema = z.object({
password: z.string(),
confirmPassword: z.string(),
}).superRefine((data, ctx) => {
if (data.password !== data.confirmPassword) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["confirmPassword"],
message: "Passwords do not match",
});
}
});
Тест:
const result = schema.safeParse({
password: "123456",
confirmPassword: "111111",
});
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path)
.toEqual(["confirmPassword"]);
}
const schema = z.string()
.transform((v) => v.toUpperCase());
const result = schema.safeParse("hello");
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe("HELLO");
}
const schema = z.string()
.transform((v) => v.trim())
.transform((v) => v.toUpperCase());
const result = schema.safeParse(" hello ");
if (result.success) {
expect(result.data).toBe("HELLO");
}
const schema = z.preprocess(
(value) => Number(value),
z.number()
);
Тест:
const result = schema.safeParse("42");
expect(result.success).toBe(true);
if (result.success) {
expect(result.data).toBe(42);
}
const schema = z.string().optional();
expect(schema.safeParse(undefined).success)
.toBe(true);
expect(schema.safeParse("hello").success)
.toBe(true);
const schema = z.string().nullable();
expect(schema.safeParse(null).success)
.toBe(true);
const schema = z.string().nullish();
expect(schema.safeParse(undefined).success)
.toBe(true);
expect(schema.safeParse(null).success)
.toBe(true);
const schema = z.string().default("guest");
const result = schema.parse(undefined);
expect(result).toBe("guest");
const schema = z.coerce.number();
Проверка:
expect(schema.parse("42")).toBe(42);
Некорректные данные:
expect(() => {
schema.parse("abc");
}).toThrow();
const schema = z.string().refine(
async (value) => {
return value !== "admin";
},
{
message: "Reserved name",
}
);
Тест:
it("проверяет async refine", async () => {
const result = await schema.safeParseAsync(
"admin"
);
expect(result.success).toBe(false);
});
await expect(
schema.parseAsync("admin")
).rejects.toThrow();
Повторяющиеся проверки удобно выносить в таблицы.
describe("EmailSchema", () => {
const schema = z.string().email();
it.each([
"a@test.com",
"b@test.com",
"user@gmail.com",
])("валидный email: %s", (email) => {
expect(
schema.safeParse(email).success
).toBe(true);
});
it.each([
"",
"abc",
"wrong-email",
])("невалидный email: %s", (email) => {
expect(
schema.safeParse(email).success
).toBe(false);
});
});
Особое внимание уделяется:
null;undefined;Пример:
const schema = z.number().min(1).max(10);
expect(schema.safeParse(1).success)
.toBe(true);
expect(schema.safeParse(10).success)
.toBe(true);
expect(schema.safeParse(0).success)
.toBe(false);
expect(schema.safeParse(11).success)
.toBe(false);
const schema = z.number();
expect(schema.safeParse(NaN).success)
.toBe(false);
expect(schema.safeParse(Infinity).success)
.toBe(true);
Запрет Infinity:
const finiteSchema = z.number().finite();
expect(
finiteSchema.safeParse(Infinity).success
).toBe(false);
const schema = z.object({
name: z.string(),
}).strict();
Тест:
const result = schema.safeParse({
name: "Alex",
age: 30,
});
expect(result.success).toBe(false);
const schema = z.object({
name: z.string(),
}).passthrough();
Тест:
const result = schema.parse({
name: "Alex",
age: 30,
});
expect(result.age).toBe(30);
const schema = z.object({
name: z.string(),
}).strip();
const result = schema.parse({
name: "Alex",
age: 30,
});
expect(result).toEqual({
name: "Alex",
});
| Метод | Поведение |
|---|---|
parse() |
Выбрасывает исключение |
safeParse() |
Возвращает объект результата |
parse() полезен при проверке выбрасывания ошибок:
expect(() => {
schema.parse({});
}).toThrow();
safeParse() удобен для детального анализа ошибок:
const result = schema.safeParse({});
Иногда удобно сохранять структуру ошибок.
const result = schema.safeParse({});
expect(result).toMatchSnapshot();
Пример snapshot:
{
"success": false,
"error": ...
}
Недостаток snapshot-подхода — высокая чувствительность к изменениям текста ошибок.
function expectSuccess(result: any) {
expect(result.success).toBe(true);
}
Использование:
expectSuccess(
schema.safeParse(validData)
);
function expectFailure(result: any) {
expect(result.success).toBe(false);
}
const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
Тест:
describe("CreateUserSchema", () => {
it("принимает корректный payload", () => {
const result =
CreateUserSchema.safeParse({
email: "user@test.com",
password: "12345678",
});
expect(result.success).toBe(true);
});
it("отклоняет короткий пароль", () => {
const result =
CreateUserSchema.safeParse({
email: "user@test.com",
password: "123",
});
expect(result.success).toBe(false);
});
});
const EnvSchema = z.object({
PORT: z.coerce.number(),
NODE_ENV: z.enum([
"development",
"production",
]),
});
Тест:
expect(
EnvSchema.safeParse({
PORT: "3000",
NODE_ENV: "production",
}).success
).toBe(true);
Хотя TypeScript проверяет типы на этапе компиляции, тестирование полезно для контроля runtime-поведения.
type User = z.infer<typeof UserSchema>;
Проверка:
const user: User = {
name: "Alex",
age: 20,
};
Плохо:
expect(result.success).toBe(false);
Лучше:
expect(result.error.issues[0].path)
.toEqual(["email"]);
Плохо:
it("валидирует всё", () => {
});
Лучше разбивать:
Недостаточно проверять только «обычные» данные.
Особенно важно тестировать:
schemas/
user.schema.ts
tests/
user.schema.test.ts
describe("UserSchema", () => {
describe("name", () => {
});
describe("age", () => {
});
describe("email", () => {
});
});
После изменения схемы тесты позволяют быстро обнаружить:
Схема часто тестируется вместе с HTTP-слоем.
Пример с Express:
app.post("/users", (req, res) => {
const result =
CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
errors: result.error.flatten(),
});
}
res.json(result.data);
});
Тест:
const response = await request(app)
.post("/users")
.send({
email: "wrong",
password: "123",
});
expect(response.status).toBe(400);
Для сложных схем применяются генераторы случайных данных.
Пример с fast-check:
import fc from "fast-check";
it("не принимает отрицательный возраст", () => {
fc.assert(
fc.property(
fc.integer({ max: -1 }),
(age) => {
const result =
z.number()
.positive()
.safeParse(age);
return result.success === false;
}
)
);
});
Такой подход помогает находить неожиданные edge-case сценарии.
Сложные схемы с множеством refine() могут создавать
нагрузку.
Простейший benchmark:
const start = performance.now();
for (let i = 0; i < 10000; i++) {
schema.safeParse(data);
}
const end = performance.now();
console.log(end - start);
Особенно важно контролировать: