Тестирование с базой данных

Тестирование с БД — integration-тесты, которые работают с реальной (но тестовой) базой данных, применяя стратегии изоляции данных между тестами: миграции, truncate, транзакции или in-memory БД.

Зачем нужно

Мокирование репозитория скрывает баги в SQL-запросах, неправильных индексах, foreign key constraints. Тесты с реальной БД ловят эти проблемы до production. Правильная изоляция обеспечивает независимость тестов и воспроизводимость.

Где используется

  • Репозитории и DAO-слой (data access objects)
  • ORM-запросы (Prisma, TypeORM, Knex, Sequelize)
  • Тестирование миграций и сидов
  • Integration-тесты API-эндпоинтов

Основной контент

Стратегии изоляции

1. TRUNCATE/DELETE перед каждым тестом — простая очистка
2. Транзакция с rollback — данные не сохраняются
3. Отдельная тестовая БД — параллельный запуск
4. In-memory БД (SQLite) — скорость без IO

Knex + SQLite in-memory

// db.ts
import knex from 'knex';

export const db = knex({
  client: 'sqlite3',
  connection: ':memory:',  // в памяти, очищается при перезапуске
  useNullAsDefault: true,
});

// userRepository.test.ts
import { db } from './db';
import { UserRepository } from './UserRepository';

const repo = new UserRepository(db);

beforeAll(async () => {
  await db.schema.createTable('users', (table) => {
    table.increments('id');
    table.string('name').notNullable;
    table.string('email').unique.notNullable;
    table.timestamps(true, true);
  });
});

afterAll(async () => {
  await db.destroy();
});

beforeEach(async () => {
  await db('users').truncate; // чистим перед каждым тестом
});

test('сохраняет пользователя', async () => {
  const user = await repo.save({ name: 'Alice', email: 'alice@test.com' });
  expect(user.id).toBeDefined();
  expect(user.name).toBe('Alice');
});

test('находит пользователя по email', async () => {
  await db('users').insert({ name: 'Bob', email: 'bob@test.com' });
  const user = await repo.findByEmail('bob@test.com');
  expect(user.name).toBe('Bob');
});

test('возвращает null если пользователь не найден', async () => {
  const user = await repo.findByEmail('nobody@test.com');
  expect(user).toBeNull();
});

Транзакция с rollback (PostgreSQL)

// Всё что происходит в тесте откатывается после завершения
beforeEach(async () => {
  await db.raw('BEGIN');
});

afterEach(async () => {
  await db.raw('ROLLBACK');
});

test('создаёт пользователя', async () => {
  await db('users').insert({ name: 'Alice', email: 'alice@test.com' });
  const users = await db('users').select('*');
  expect(users).toHaveLength(1);
  // ROLLBACK откатывает вставку
});

Prisma + тестовая БД

// prisma/schema.prisma использует DATABASE_URL из env
// В .env.test: DATABASE_URL="postgresql://user:pass@localhost:5432/myapp_test"

// jest.config.js
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/myapp_test';

// test setup
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

beforeEach(async () => {
  await prisma.user.deleteMany; // очищаем тестовые данные
});

afterAll(async () => {
  await prisma.$disconnect;
});

test('создаёт пользователя через Prisma', async () => {
  const user = await prisma.user.create({
    data: { name: 'Alice', email: 'alice@test.com' },
  });
  expect(user.id).toBeDefined();
});

Тестирование миграций

test('миграция создаёт таблицу users', async () => {
  await db.migrate.latest;
  const hasTable = await db.schema.hasTable('users');
  expect(hasTable).toBe(true);
});

Частые ошибки

  • Тесты не изолированы — данные одного теста влияют на другой; всегда используй TRUNCATE/ROLLBACK/deleteMany в beforeEach
  • Тесты с реальной production БД — никогда; используй отдельную тестовую БД или in-memory
  • Параллельный запуск без изоляции — при --maxWorkers > 1 тесты конкурируют за данные; для БД-тестов используй --runInBand

Связанные темы

Ресурсы