Интеграционные тесты проверяют взаимодействие нескольких частей приложения одновременно: HTTP-слоя, middleware, бизнес-логики, базы данных, очередей, внешних API и механизмов валидации. В контексте Zod интеграционные тесты особенно важны, поскольку ошибки схем часто проявляются не на уровне отдельных функций, а при прохождении полного запроса через приложение.
Типичные проблемы, выявляемые интеграционными тестами:
Zod в интеграционных тестах выступает одновременно:
import express fr om "express";
import { z } fr om "zod";
const app = express();
app.use(express.json());
const CreateUserSchema = z.object({
email: z.string().email(),
age: z.number().int().positive(),
});
app.post("/users", (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: result.error.flatten(),
});
}
res.status(201).json({
id: 1,
...result.data,
});
});
npm install --save-dev jest supertest
import request fr om "supertest";
import { app } from "./app";
describe("POST /users", () => {
test("создание пользователя с валидными данными", async () => {
const response = await request(app)
.post("/users")
.send({
email: "admin@test.com",
age: 25,
});
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
email: "admin@test.com",
age: 25,
});
});
});
Тест проверяет:
test("ошибка при некорректном email", async () => {
const response = await request(app)
.post("/users")
.send({
email: "invalid-email",
age: 20,
});
expect(response.status).toBe(400);
expect(response.body.error.fieldErrors.email).toBeDefined();
});
test("ошибка при отсутствии age", async () => {
const response = await request(app)
.post("/users")
.send({
email: "user@test.com",
});
expect(response.status).toBe(400);
expect(response.body.error.fieldErrors.age).toBeDefined();
});
const OrderSchema = z.object({
customer: z.object({
name: z.string(),
email: z.string().email(),
}),
items: z.array(
z.object({
productId: z.number(),
quantity: z.number().positive(),
})
),
shipping: z.object({
city: z.string(),
zip: z.string(),
}),
});
test("создание заказа", async () => {
const response = await request(app)
.post("/orders")
.send({
customer: {
name: "Alex",
email: "alex@test.com",
},
items: [
{
productId: 10,
quantity: 2,
},
],
shipping: {
city: "Berlin",
zip: "10115",
},
});
expect(response.status).toBe(201);
});
Интеграционные тесты должны покрывать не только успешные кейсы, но и все возможные нарушения схемы.
test("ошибка при строковом quantity", async () => {
const response = await request(app)
.post("/orders")
.send({
customer: {
name: "Alex",
email: "alex@test.com",
},
items: [
{
productId: 10,
quantity: "2",
},
],
shipping: {
city: "Berlin",
zip: "10115",
},
});
expect(response.status).toBe(400);
});
const OrderSchema = z.object({
items: z
.array(ItemSchema)
.min(1),
});
Тест:
test("ошибка при пустом массиве товаров", async () => {
const response = await request(app)
.post("/orders")
.send({
items: [],
});
expect(response.status).toBe(400);
});
const UserSchema = z.object({
age: z.string().transform(Number),
});
app.post("/users", (req, res) => {
const parsed = UserSchema.parse(req.body);
res.json(parsed);
});
Тест:
test("transform преобразует строку в число", async () => {
const response = await request(app)
.post("/users")
.send({
age: "42",
});
expect(response.body.age).toBe(42);
expect(typeof response.body.age).toBe("number");
});
Интеграционный тест здесь критически важен, поскольку проверяется не только схема, но и итоговое поведение HTTP API.
const Schema = z.object({
age: z.preprocess(
value => Number(value),
z.number()
),
});
test("preprocess приводит строку к числу", async () => {
const response = await request(app)
.post("/users")
.send({
age: "55",
});
expect(response.status).toBe(200);
expect(response.body.age).toBe(55);
});
const PasswordSchema = z.object({
password: z.string().min(8),
}).refine(
data => /[A-Z]/.test(data.password),
{
message: "Password must contain uppercase letter",
path: ["password"],
}
);
test("ошибка при отсутствии заглавной буквы", async () => {
const response = await request(app)
.post("/register")
.send({
password: "weakpass",
});
expect(response.status).toBe(400);
expect(
response.body.error.fieldErrors.password
).toContain(
"Password must contain uppercase letter"
);
});
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 do not match",
path: ["confirmPassword"],
});
}
});
test("ошибка несовпадения паролей", async () => {
const response = await request(app)
.post("/register")
.send({
password: "secret123",
confirmPassword: "secret456",
});
expect(response.status).toBe(400);
expect(
response.body.error.fieldErrors.confirmPassword
).toContain("Passwords do not match");
});
const QuerySchema = z.object({
page: z.coerce.number().min(1),
lim it: z.coerce.number().max(100),
});
const validateQuery = (schema) => {
return (req, res, next) => {
const result = schema.safeParse(req.query);
if (!result.success) {
return res.status(400).json(result.error);
}
req.query = result.data;
next();
};
};
test("валидные query-параметры", async () => {
const response = await request(app)
.get("/users?page=1&limit=20");
expect(response.status).toBe(200);
});
test("ошибка при отрицательной странице", async () => {
const response = await request(app)
.get("/users?page=-1&limit=20");
expect(response.status).toBe(400);
});
const ParamsSchema = z.object({
id: z.coerce.number().int().positive(),
});
const validateParams = (schema) => {
return (req, res, next) => {
const result = schema.safeParse(req.params);
if (!result.success) {
return res.status(400).json(result.error);
}
req.params = result.data;
next();
};
};
test("валидный id", async () => {
const response = await request(app)
.get("/users/10");
expect(response.status).toBe(200);
});
test("ошибка при нечисловом id", async () => {
const response = await request(app)
.get("/users/abc");
expect(response.status).toBe(400);
});
Валидация ответов — важный компонент интеграционных тестов. Она предотвращает случайное изменение контрактов API.
const UserResponseSchema = z.object({
id: z.number(),
email: z.string().email(),
age: z.number(),
});
test("ответ соответствует контракту", async () => {
const response = await request(app)
.get("/users/1");
const parsed = UserResponseSchema.parse(
response.body
);
expect(parsed.id).toBeDefined();
});
Zod позволяет строить единые схемы для backend и frontend.
export const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
});
res.json(UserSchema.parse(user));
const user = UserSchema.parse(await response.json());
test("backend возвращает совместимый контракт", async () => {
const response = await request(app)
.get("/users/1");
expect(() => {
UserSchema.parse(response.body);
}).not.toThrow();
});
const validate = (schema) => {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
errors: result.error.flatten(),
});
}
req.body = result.data;
next();
};
};
app.post(
"/users",
validate(UserSchema),
controller
);
test("middleware блокирует невалидный запрос", async () => {
const response = await request(app)
.post("/users")
.send({});
expect(response.status).toBe(400);
});
const UserSchema = z.object({
email: z.string().email(),
}).refine(
async data => {
const exists = await db.userExists(data.email);
return !exists;
},
{
message: "User already exists",
}
);
const parsed = await UserSchema.parseAsync(req.body);
test("ошибка при существующем email", async () => {
await createUser({
email: "admin@test.com",
});
const response = await request(app)
.post("/users")
.send({
email: "admin@test.com",
});
expect(response.status).toBe(400);
});
test("валидный пользователь сохраняется в БД", async () => {
const response = await request(app)
.post("/users")
.send({
email: "user@test.com",
age: 20,
});
const user = await prisma.user.findUnique({
wh ere: {
email: "user@test.com",
},
});
expect(user).not.toBeNull();
});
test("невалидный пользователь не сохраняется", async () => {
await request(app)
.post("/users")
.send({
email: "wrong-email",
age: 20,
});
const user = await prisma.user.findUnique({
wh ere: {
email: "wrong-email",
},
});
expect(user).toBeNull();
});
test("snapshot ошибки валидации", async () => {
const response = await request(app)
.post("/users")
.send({});
expect(response.body).toMatchSnapshot();
});
Такой подход помогает обнаруживать:
При использовании zod-openapi интеграционные тесты
помогают контролировать актуальность документации.
test("OpenAPI содержит UserSchema", () => {
expect(document.components.schemas.User)
.toBeDefined();
});
Частая ошибка:
snake_case;camelCase.test("формат ответа соответствует frontend", async () => {
const response = await request(app)
.get("/users/1");
expect(response.body.createdAt)
.toBeDefined();
});
const Schema = z.object({
email: z.string(),
}).strict();
test("лишние поля запрещены", async () => {
const response = await request(app)
.post("/users")
.send({
email: "user@test.com",
role: "admin",
});
expect(response.status).toBe(400);
});
const Schema = z.object({
email: z.string(),
}).passthrough();
const Schema = z.object({
email: z.string(),
}).strip();
test("лишние поля удаляются", async () => {
const response = await request(app)
.post("/users")
.send({
email: "user@test.com",
role: "admin",
});
expect(response.body.role)
.toBeUndefined();
});
const PaymentSchema = z.discriminatedUnion(
"type",
[
z.object({
type: z.literal("card"),
cardNumber: z.string(),
}),
z.object({
type: z.literal("paypal"),
email: z.string().email(),
}),
]
);
test("paypal-платеж проходит валидацию", async () => {
const response = await request(app)
.post("/payment")
.send({
type: "paypal",
email: "pay@test.com",
});
expect(response.status).toBe(200);
});
const UploadSchema = z.object({
title: z.string(),
});
test("загрузка файла", async () => {
const response = await request(app)
.post("/upload")
.field("title", "Document")
.attach("file", "__tests__/file.pdf");
expect(response.status).toBe(200);
});
Иногда API возвращает:
Date;BigInt;Интеграционные тесты помогают выявлять несовместимости.
const Schema = z.object({
createdAt: z.string().datetime(),
});
test("дата сериализуется корректно", async () => {
const response = await request(app)
.get("/posts/1");
expect(() => {
Schema.parse(response.body);
}).not.toThrow();
});
Интеграционные тесты особенно ценны после:
src/
tests/
integration/
users/
create-user.test.ts
update-user.test.ts
orders/
create-order.test.ts
Каждый тест должен проверять один конкретный контракт.
Плохо:
test("user flow", async () => {
// десятки проверок
});
Хорошо:
test("ошибка при пустом email", async () => {});
test("ошибка при неверном age", async () => {});
test("успешное создание пользователя", async () => {});
Рекомендуемые практики:
Проблема:
schema.parse(data);
При ошибке тест падает исключением до проверки результата.
Для middleware обычно безопаснее:
schema.safeParse(data);
Плохо:
expect(response.status).toBe(200);
Правильно:
expect(() => {
ResponseSchema.parse(response.body);
}).not.toThrow();
Нужно обязательно тестировать:
Один из наиболее надёжных подходов:
Такой подход обеспечивает: