Интеграционное тестирование JavaScript с Docker: от теории к продвинутой практике
Интеграционное тестирование — критически важный этап разработки, который проверяет взаимодействие между различными модулями и внешними сервисами. Для JavaScript-приложений на Node.js Docker становится незаменимым инструментом, позволяющим создавать изолированные, воспроизводимые среды для тестирования. В этой статье мы рассмотрим продвинутые техники настройки интеграционных тестов с Docker, включая работу с базами данных, очередями и оптимизацию для CI/CD.
Почему Docker для интеграционных тестов?
Традиционные подходы к интеграционному тестированию (моки, stubs) не всегда отражают реальное поведение системы. Docker решает эту проблему, предоставляя:
- Изоляцию: каждый тест запускается в чистом контейнере без влияния на другие тесты или локальную среду.
- Воспроизводимость: одинаковые условия выполнения на машине разработчика и в CI/CD.
- Масштабируемость: возможность параллельного запуска тестов с разными версиями зависимостей (PostgreSQL, Redis, Elasticsearch).
- Быстрое восстановление: пересоздание контейнеров занимает секунды, а не часы настройки.
Настройка продвинутого тестового окружения
Рассмотрим типичное Node.js приложение, которое использует PostgreSQL и Redis. Вместо простого docker-compose.yml создадим гибкую конфигурацию с поддержкой параллельных тестов и health checks.
Шаг 1: Docker Compose с динамическими портами
Чтобы избежать конфликтов портов при параллельном запуске, используем динамическое выделение:
# docker-compose.test.yml
version: '3.8'
services:
postgres_test:
image: postgres:15-alpine
environment:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
ports:
- "0:5432" # Динамический порт
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test_user -d test_db"]
interval: 2s
timeout: 5s
retries: 10
tmpfs: /var/lib/postgresql/data # Данные в RAM для скорости
redis_test:
image: redis:7-alpine
ports:
- "0:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 2s
timeout: 5s
retries: 10
Шаг 2: Утилиты для запуска тестов с Docker
Создадим класс-помощник, который автоматически находит динамические порты и ожидает готовности сервисов:
// test/setup/docker-manager.js
const { DockerComposeEnvironment, Wait } = require('testcontainers');
const { Client } = require('pg');
const Redis = require('ioredis');
class DockerTestManager {
constructor() {
this.environment = null;
this.config = {
pgPort: null,
redisPort: null
};
}
async start() {
this.environment = await new DockerComposeEnvironment(
'.',
'docker-compose.test.yml'
).withWaitStrategy('postgres_test', Wait.forHealthCheck())
.withWaitStrategy('redis_test', Wait.forHealthCheck())
.up();
// Извлекаем маппинг портов
const pgContainer = this.environment.getContainer('postgres_test');
const redisContainer = this.environment.getContainer('redis_test');
this.config.pgPort = pgContainer.getMappedPort(5432);
this.config.redisPort = redisContainer.getMappedPort(6379);
return this.config;
}
async stop() {
if (this.environment) {
await this.environment.down();
}
}
}
module.exports = DockerTestManager;
Продвинутые сценарии интеграционного тестирования
Теперь перейдем к конкретным тестам, которые проверяют реальное взаимодействие компонентов.
Тестирование записи и чтения из PostgreSQL
Проверим, что наше приложение корректно сохраняет и извлекает данные:
// test/integration/user.repository.test.js
const { Client } = require('pg');
const DockerTestManager = require('../setup/docker-manager');
describe('User Repository Integration', () => {
let dockerManager;
let pgClient;
beforeAll(async () => {
dockerManager = new DockerTestManager();
const config = await dockerManager.start();
pgClient = new Client({
host: 'localhost',
port: config.pgPort,
database: 'test_db',
user: 'test_user',
password: 'test_pass'
});
await pgClient.connect();
// Создаем таблицу для тестов
await pgClient.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)
`);
});
afterAll(async () => {
await pgClient.end();
await dockerManager.stop();
});
beforeEach(async () => {
await pgClient.query('TRUNCATE users RESTART IDENTITY CASCADE');
});
test('should create a new user', async () => {
const result = await pgClient.query(
'INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *',
['test@example.com', 'Test User']
);
expect(result.rows[0]).toMatchObject({
email: 'test@example.com',
name: 'Test User'
});
expect(result.rows[0].id).toBeDefined();
});
test('should enforce unique email constraint', async () => {
await pgClient.query(
'INSERT INTO users (email, name) VALUES ($1, $2)',
['duplicate@example.com', 'First']
);
await expect(
pgClient.query(
'INSERT INTO users (email, name) VALUES ($1, $2)',
['duplicate@example.com', 'Second']
)
).rejects.toThrow();
});
});
Тестирование кэширования с Redis
Проверим, что кэш работает корректно и сбрасывается при необходимости:
// test/integration/cache.service.test.js
const Redis = require('ioredis');
const DockerTestManager = require('../setup/docker-manager');
describe('Cache Service Integration', () => {
let dockerManager;
let redisClient;
beforeAll(async () => {
dockerManager = new DockerTestManager();
const config = await dockerManager.start();
redisClient = new Redis({
host: 'localhost',
port: config.redisPort
});
});
afterAll(async () => {
await redisClient.quit();
await dockerManager.stop();
});
beforeEach(async () => {
await redisClient.flushall();
});
test('should set and get cached value', async () => {
await redisClient.set('user:1', JSON.stringify({ id: 1, name: 'Alice' }));
const cached = await redisClient.get('user:1');
expect(JSON.parse(cached)).toEqual({ id: 1, name: 'Alice'