Моки для базы данных
Мок базы данных — это замена реального DB-клиента тестовым двойником (spy, mock, stub), который имитирует поведение репозитория без обращения к реальной базе данных, что позволяет тестировать бизнес-логику быстро и изолированно.
Зачем нужно
Unit-тесты должны быть быстрыми и не зависеть от внешних ресурсов. Тест, который обращается к реальной PostgreSQL, требует запущенного сервера, чистой схемы и данных — это медленно и ненадёжно. Мокирование репозитория позволяет тестировать Service-слой изолированно: проверять логику, не трогая БД.
Где используется
- Unit-тесты Service-слоя (UsersService, OrdersService)
- Тесты с непредсказуемыми данными (проверка граничных случаев)
- CI без доступа к реальной БД
- TDD — писать тесты до реализации Repository
Основной контент
Мок через jest.mock (Repository)
// services/UsersService.js
const UsersRepository = require('../repositories/UsersRepository');
class UsersService {
async getById(id) {
const user = await UsersRepository.findById(id);
if (!user) throw new Error('User not found');
return user;
}
async create({ name, email }) {
const existing = await UsersRepository.findByEmail(email);
if (existing) throw new Error('Email already in use');
return UsersRepository.create({ name, email });
}
}
module.exports = new UsersService();
// __tests__/UsersService.test.js
jest.mock('../repositories/UsersRepository');
const UsersRepository = require('../repositories/UsersRepository');
const UsersService = require('../services/UsersService');
describe('UsersService.getById', () => {
it('возвращает пользователя', async () => {
const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' };
UsersRepository.findById.mockResolvedValue(mockUser);
const user = await UsersService.getById(1);
expect(user).toEqual(mockUser);
expect(UsersRepository.findById).toHaveBeenCalledWith(1);
});
it('бросает ошибку если пользователь не найден', async () => {
UsersRepository.findById.mockResolvedValue(null);
await expect(UsersService.getById(99)).rejects.toThrow('User not found');
});
});
describe('UsersService.create', () => {
it('не создаёт при дублировании email', async () => {
UsersRepository.findByEmail.mockResolvedValue({ id: 1 }); // уже существует
await expect(UsersService.create({ name: 'Bob', email: 'alice@example.com' }))
.rejects.toThrow('Email already in use');
expect(UsersRepository.create).not.toHaveBeenCalled();
});
});
Мок Prisma Client
// __mocks__/@prisma/client.js (автомок)
const prismaMock = {
user: {
findUnique: jest.fn,
findMany: jest.fn,
create: jest.fn,
update: jest.fn,
delete: jest.fn
},
$transaction: jest.fn((fn) => fn(prismaMock))
};
module.exports = { PrismaClient: jest.fn( => prismaMock) };
// В тесте
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
prisma.user.findUnique.mockResolvedValue({ id: 1, email: 'alice@example.com' });
mongodb-memory-server — in-memory MongoDB
npm install --save-dev mongodb-memory-server
// jest.setup.js
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
let mongod;
beforeAll(async () => {
mongod = await MongoMemoryServer.create;
await mongoose.connect(mongod.getUri);
});
afterEach(async () => {
// Очистить все коллекции
const collections = await mongoose.connection.db.collections;
for (const collection of collections) {
await collection.deleteMany({});
}
});
afterAll(async () => {
await mongoose.disconnect();
await mongod.stop();
});
// В тесте — реальные Mongoose-операции, но in-memory
const User = require('../models/User');
it('создаёт пользователя', async () => {
const user = await User.create({ name: 'Alice', email: 'alice@example.com' });
expect(user._id).toBeDefined();
expect(user.name).toBe('Alice');
});
Мок через Dependency Injection
// Если Service принимает Repository через конструктор — мокировать проще
class UsersService {
constructor(usersRepository) {
this.repo = usersRepository;
}
async getById(id) {
const user = await this.repo.findById(id);
if (!user) throw new Error('Not found');
return user;
}
}
// В тесте
const mockRepo = { findById: jest.fn };
const service = new UsersService(mockRepo);
mockRepo.findById.mockResolvedValue({ id: 1, name: 'Alice' });
const user = await service.getById(1);
Частые ошибки
- Мокировать то, чем не владеешь — не мокировать
pg,mongooseнапрямую; мокировать свой Repository - Не сбрасывать моки между тестами —
jest.clearAllMocksилиclearMocks: trueв jest.config - Тестировать только успешные случаи — всегда мокировать
null,Error, граничные значения - Мокировать в интеграционных тестах — интеграционные тесты должны использовать реальную (тестовую) БД