Генерация тестовых данных

Библиотека Zod предназначена не только для валидации входящих данных, но и для построения полноценной инфраструктуры вокруг схем: генерации типов, трансформаций, безопасного парсинга, мокирования и автоматического создания тестовых наборов. Генерация тестовых данных особенно полезна при:

  • написании unit-тестов;
  • тестировании API;
  • создании seed-данных;
  • проверке edge-case сценариев;
  • тестировании форм;
  • генерации случайных объектов для property-based testing.

Сам 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),
});

На основе этой схемы можно:

  • валидировать реальные данные;
  • автоматически генерировать корректные объекты;
  • строить mock-данные;
  • создавать случайные тестовые значения.

Основные библиотеки для генерации данных

Наиболее популярные решения:

Библиотека Назначение
@faker-js/faker Генерация случайных данных
zod-fixture Создание объектов из схем Zod
zod-mock Автоматическая генерация mock-данных
fast-check Property-based testing
@anatine/zod-mock Продвинутое мокирование
chance Генератор случайных значений

Генерация данных через faker

Установка

npm install zod @faker-js/faker

Ручное построение mock-объектов

Самый простой подход — комбинировать 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 }),
};

Автоматическая генерация через zod-mock

Установка

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"]
}

Генерация union-типов

const ResultSchema = z.union([
  z.string(),
  z.number(),
]);

const mock = generateMock(ResultSchema);

Библиотека выбирает один из вариантов:

"string"

или

123

Генерация enum

const RoleSchema = z.enum([
  "admin",
  "user",
  "moderator",
]);

const mock = generateMock(RoleSchema);

Результат:

"admin"

Работа с optional и nullable

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",
  },
});

Генерация через transform

Схемы Zod могут содержать трансформации.

const UserSchema = z.object({
  name: z.string(),
}).transform(data => ({
  ...data,
  slug: data.name.toLowerCase(),
}));

Важно понимать различие:

  • генератор создает входные данные;
  • transform применяется после parse.

Пример:

const input = {
  name: "John",
};

const result = UserSchema.parse(input);

console.log(result);

Результат:

{
  name: "John",
  slug: "john"
}

Генерация данных для API-тестов

Схемы часто используются как контракты 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);
});

Seed-данные

Генерация сидов для базы данных:

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

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;
  })
);

Интеграция fast-check и Zod

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;
    }
  )
);

Генерация edge-case значений

Тестовые данные должны покрывать:

  • пустые строки;
  • null;
  • undefined;
  • NaN;
  • Infinity;
  • большие числа;
  • Unicode;
  • очень длинные строки;
  • вложенные структуры;
  • пустые массивы.

Пример:

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

Метод 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(),
};

Генерация UUID

const UserSchema = z.object({
  id: z.string().uuid(),
});

Mock:

const user = {
  id: faker.string.uuid(),
};

Генерация URL

const SiteSchema = z.object({
  url: z.string().url(),
});
const site = {
  url: faker.internet.url(),
};

Генерация сложных схем

Дискриминирующие 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 shape = generateMock(ShapeSchema);

Результат:

{
  type: "circle",
  radius: 100
}

Lazy-схемы

Рекурсивные структуры:

const CategorySchema: z.ZodType<any> = z.object({
  name: z.string(),
  children: z.array(
    z.lazy(() => CategorySchema)
  ),
});

Такие схемы сложнее для генерации.

Некоторые библиотеки:

  • не поддерживают recursion;
  • создают бесконечную вложенность;
  • требуют ограничения глубины.

Ограничение глубины рекурсии

Пример ручной генерации:

function createCategory(depth = 0): any {
  if (depth > 3) {
    return {
      name: "Leaf",
      children: [],
    };
  }

  return {
    name: faker.commerce.department(),
    children: [
      createCategory(depth + 1),
    ],
  };
}

Генерация тестовых данных в Vitest

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);
  });
});

Генерация тестовых данных в Jest

describe("User schema", () => {
  test("generated user is valid", () => {
    const user = {
      id: faker.number.int(),
      name: faker.person.fullName(),
    };

    expect(() => {
      UserSchema.parse(user);
    }).not.toThrow();
  });
});

Snapshot-тестирование

test("user snapshot", () => {
  faker.seed(1);

  const user = {
    id: faker.number.int(),
    name: faker.person.fullName(),
  };

  expect(user).toMatchSnapshot();
});

Генерация данных для e2e-тестов

Playwright:

const user = {
  email: faker.internet.email(),
  password: faker.internet.password(),
};

Cypress:

cy.request("POST", "/users", user);

Стратегии генерации

Полностью случайная генерация

Плюсы:

  • широкий охват;
  • поиск неожиданных багов.

Минусы:

  • нестабильность тестов;
  • сложность воспроизведения.

Детерминированная генерация

Плюсы:

  • повторяемость;
  • стабильные snapshot-тесты.

Минусы:

  • меньший охват сценариев.

Гибридный подход

Наиболее распространенная стратегия:

  • фиксированный seed;
  • периодическая смена seed;
  • ручной набор edge-case данных;
  • случайная генерация внутри ограничений схемы.

Производительность

При генерации больших объемов данных могут возникать проблемы:

  • медленный parse;
  • глубокая рекурсия;
  • большое потребление памяти;
  • медленные refine-функции.

Пример оптимизации:

const ParsedUserSchema = UserSchema.strict();

Строгие схемы быстрее выявляют ошибки структуры.


Проверка покрытия схем

Полезная практика — тестировать:

  • корректные данные;
  • некорректные данные;
  • граничные значения;
  • случайные значения;
  • пустые объекты;
  • лишние поля;
  • null и undefined.

Комбинирование factory-функций

Практика крупных проектов — использование фабрик.

function createUser(overrides = {}) {
  return {
    id: faker.number.int(),
    name: faker.person.fullName(),
    email: faker.internet.email(),
    ...overrides,
  };
}

Использование:

const admin = createUser({
  role: "admin",
});

Проверка фабрик через Zod

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-генератор может создать слишком короткую строку.


Игнорирование refine

z.string().refine(...)

Большинство генераторов не умеют анализировать пользовательскую логику.


Сложные transform

.transform()

Трансформации часто выполняются уже после генерации.


Рекурсивные схемы

z.lazy()

Могут вызывать бесконечную генерацию.


Практика организации генераторов

Распространенная структура проекта:

src/
  schemas/
  factories/
  mocks/
  tests/

Пример:

factories/
  user.factory.ts
  post.factory.ts
  comment.factory.ts

Пример полноценной factory-системы

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(),
    })
  ),
});

Интеграция с Prisma

Zod часто используется вместе с Prisma для генерации seed-данных.

await prisma.user.create({
  data: createUser(),
});

Генерация тестовых данных как часть CI

Автоматическая генерация помогает:

  • обнаруживать ошибки схем;
  • выявлять проблемы сериализации;
  • проверять backward compatibility;
  • тестировать миграции API;
  • находить нестабильные refine-условия.

Практика безопасной генерации

Рекомендуемые правила:

  • всегда валидировать сгенерированные данные;
  • использовать deterministic seed;
  • отдельно тестировать invalid-сценарии;
  • покрывать edge-cases;
  • ограничивать глубину recursion;
  • использовать фабрики вместо хаотичных mock-объектов;
  • хранить схемы и генераторы рядом;
  • использовать TypeScript inference через z.infer.