Интеграционное тестирование JavaScript с Docker: полное руководство
Интеграционное тестирование — критически важный этап разработки на JavaScript (Node.js). Оно проверяет взаимодействие вашего приложения с внешними сервисами: базами данных, API, кэшами и очередями. Использование Docker для этих целей позволяет изолировать тестовую среду, гарантировать её воспроизводимость и избежать проблем с "это работает на моей машине". В этой статье мы разберём, как настроить интеграционное тестирование JavaScript-приложений с помощью Docker, Docker Compose и популярных тестовых фреймворков.
Зачем нужен Docker для интеграционного тестирования?
Традиционное интеграционное тестирование часто сталкивается с проблемами: зависимости могут отличаться на разных машинах разработчиков, базы данных могут быть в разных состояниях, а настройка окружения отнимает часы. Docker решает эти проблемы, предоставляя:
- Изоляцию: каждый тестовый запуск происходит в чистом контейнере.
- Воспроизводимость: одинаковое окружение для всех разработчиков и CI/CD.
- Скорость: контейнеры запускаются за секунды, а не минуты.
- Управление зависимостями: легко поднять PostgreSQL, Redis, RabbitMQ и другие сервисы.
Настройка тестовой среды с Docker Compose
Начнём с создания проекта. Допустим, у нас есть простое Node.js приложение, которое сохраняет данные в PostgreSQL. Создадим файл docker-compose.test.yml:
version: '3.8'
services:
test-runner:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
- db
- redis
environment:
- NODE_ENV=test
- DATABASE_URL=postgresql://testuser:testpass@db:5432/testdb
- REDIS_URL=redis://redis:6379
volumes:
- ./coverage:/app/coverage
db:
image: postgres:15-alpine
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
POSTGRES_DB: testdb
ports:
- "5432"
tmpfs:
- /var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379"
Обратите внимание на tmpfs для PostgreSQL — это гарантирует, что после завершения тестов данные не сохранятся, и каждый запуск будет начинаться с чистой базы.
Dockerfile для тестов
Создадим Dockerfile.test, который оптимизирован для тестирования, а не для продакшена:
FROM node:18-alpine
WORKDIR /app
# Копируем только package.json для кэширования слоя зависимостей
COPY package*.json ./
RUN npm ci --only=production && npm ci --only=dev
# Копируем исходный код
COPY . .
# Устанавливаем wait-for-it для ожидания готовности сервисов
ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh
CMD ["sh", "-c", "/wait-for-it.sh db:5432 -- /wait-for-it.sh redis:6379 -- npm test"]
Написание интеграционных тестов на JavaScript
Теперь напишем сами тесты. Используем Jest и Supertest для тестирования API. Создадим файл tests/integration/user.test.js:
const request = require('supertest');
const { app } = require('../src/app');
const { Pool } = require('pg');
describe('User API Integration Tests', () => {
let pool;
beforeAll(async () => {
// Создаём пул соединений к тестовой БД
pool = new Pool({
connectionString: process.env.DATABASE_URL
});
// Выполняем миграции перед тестами
const migrationSQL = `
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
`;
await pool.query(migrationSQL);
});
afterAll(async () => {
// Очищаем и закрываем соединение
await pool.query('DROP TABLE IF EXISTS users');
await pool.end();
});
describe('POST /api/users', () => {
it('should create a new user and return 201', async () => {
const newUser = {
name: 'Иван Петров',
email: 'ivan@example.com'
};
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(newUser.name);
expect(response.body.email).toBe(newUser.email);
});
it('should return 409 for duplicate email', async () => {
const user = {
name: 'Петр Иванов',
email: 'petr@example.com'
};
// Сначала создаём пользователя
await request(app)
.post('/api/users')
.send(user)
.expect(201);
// Пытаемся создать с тем же email
const response = await request(app)
.post('/api/users')
.send(user)
.expect(409);
expect(response.body.error).toBe('Email already exists');
});
});
describe('GET /api/users/:id', () => {
it('should return user by ID', async () => {
// Создаём тестового пользователя
const createRes = await request(app)
.post('/api/users')
.send({ name: 'Тест', email: 'test@example.com' })
.expect(201);
const userId = createRes.body.id;
const response = await request(app)
.get(`/api/users/${userId}`)
.expect(200);
expect(response.body.name).toBe('Тест');
expect(response.body.email).toBe('test@example.com');
});
it('should return 404 for non-existent user', async () => {
const response = await request(app)
.get('/api/users/99999')
.expect(404);
expect(response.body.error).toBe('User not found');
});
});
});
Запуск тестов и работа с отчётами
Для запуска интеграционных тестов используем команду:
docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit
Флаг --abort-on-container-exit гарантирует, что если тесты упадут, все контейнеры остановятся. Для получения отчётов о покрытии кода добавим в package.json:
{
"scripts": {
"test": "jest --coverage --forceExit --detectOpenHandles",
"test:integration": "docker-compose -f docker-compose.test.yml up --build --abort-on-container-exit"
}
}