Тестирование с базой данных
Тестирование с БД — 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