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 или отдельные транзакции
  • Тестировать реализацию вместо поведения — проверять статус-код и форму ответа, а не вызов внутреннего метода

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

Ресурсы