Integration Testing в Node.js
Интеграционное тестирование проверяет взаимодействие нескольких слоёв приложения вместе (HTTP-обработчик + сервис + база данных), в отличие от unit-тестов, которые тестируют каждый модуль изолированно.
Зачем нужно
Unit-тесты не обнаруживают ошибки на стыке слоёв: неправильный SQL-запрос, некорректный маппинг ответа API, проблемы с middleware. Интеграционные тесты запускают настоящий HTTP-сервер против тестовой базы данных и проверяют реальные запросы/ответы, давая высокую уверенность в работоспособности системы.
Где используется
- Тестирование REST API (статусы, заголовки, тело ответа)
- Проверка работы auth middleware с реальными токенами
- CRUD-операции против тестовой БД (PostgreSQL, MongoDB in-memory)
- CI/CD — запуск перед деплоем в staging
Основной контент
Стек: Jest + Supertest
npm install --save-dev jest supertest
// package.json
{
"scripts": {
"test": "jest --runInBand",
"test:watch": "jest --watch"
},
"jest": {
"testEnvironment": "node"
}
}
Базовый тест Express API
// app.js — экспортируем app без listen
const express = require('express');
const app = express;
app.use(express.json());
app.get('/api/health', (req, res) => {
res.json({ status: 'ok' });
});
app.post('/api/users', async (req, res) => {
const { name, email } = req.body;
if (!name || !email) return res.status(400).json({ error: 'name and email required' });
const user = await UserService.create({ name, email });
res.status(201).json(user);
});
module.exports = app;
// __tests__/users.test.js
const request = require('supertest');
const app = require('../app');
describe('POST /api/users', () => {
it('201 — создаёт пользователя', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'Alice', email: 'alice@example.com' })
.expect(201)
.expect('Content-Type', /json/);
expect(res.body).toMatchObject({ name: 'Alice', email: 'alice@example.com' });
expect(res.body.id).toBeDefined();
});
it('400 — не создаёт без email', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: 'Bob' })
.expect(400);
expect(res.body.error).toBe('name and email required');
});
});
Тесты с авторизацией
describe('GET /api/profile', () => {
let token;
beforeAll(async () => {
// Логинимся один раз перед всеми тестами
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'test@example.com', password: 'password123' });
token = res.body.token;
});
it('200 — возвращает профиль авторизованному пользователю', async () => {
await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
});
it('401 — без токена', async () => {
await request(app).get('/api/profile').expect(401);
});
});
Подготовка БД для тестов
// jest.setup.js
const { db } = require('./src/db');
beforeAll(async () => {
await db.migrate.latest; // применить миграции
});
beforeEach(async () => {
await db('users').truncate; // чистая таблица перед каждым тестом
});
afterAll(async () => {
await db.destroy(); // закрыть соединение
});
// package.json
{
"jest": {
"globalSetup": "./jest.setup.js",
"testEnvironment": "node"
}
}
Использование моков для внешних сервисов
// Мокаем внешний сервис, но БД оставляем реальной
jest.mock('../services/emailService', () => ({
sendWelcomeEmail: jest.fn.mockResolvedValue(true)
}));
it('отправляет письмо при регистрации', async () => {
const emailService = require('../services/emailService');
await request(app).post('/api/register').send({ email: 'x@x.com', password: '123' });
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith('x@x.com');
});
Частые ошибки
- Вызов
app.listenвнутри модуля — Supertest сам поднимает сервер;listenвызовет конфликт портов - Не закрывать БД-соединение — Jest зависает после тестов;
afterAll( => db.destroy()) - Параллельные тесты с общей БД — гонка данных; использовать
--runInBandили отдельные транзакции - Тестировать реализацию вместо поведения — проверять статус-код и форму ответа, а не вызов внутреннего метода