Интеграционное тестирование TypeScript с Docker: продвинутый уровень
Интеграционное тестирование — ключевой этап проверки взаимодействия компонентов системы. Для TypeScript-приложений, особенно микросервисной архитектуры, Docker становится незаменимым инструментом. В этой статье мы рассмотрим продвинутые техники настройки и выполнения интеграционных тестов с использованием Docker, которые помогут вам повысить надёжность и скорость разработки.
1. Почему Docker для интеграционного тестирования TypeScript?
Docker позволяет изолировать окружения тестов, гарантируя их воспроизводимость. Вместо ручной настройки баз данных, очередей сообщений и других зависимостей, вы запускаете их в контейнерах. Это особенно важно для TypeScript-проектов, где типизация может скрывать ошибки времени выполнения, которые всплывают именно при интеграции.
- Изоляция: каждый тест запускается в чистом окружении
- Скорость: контейнеры запускаются за секунды
- Параллелизм: можно одновременно тестировать разные версии зависимостей
2. Настройка Docker Compose для тестовой среды
Создадим docker-compose.test.yml, который поднимает PostgreSQL, Redis и само приложение для тестов:
version: '3.8'
services:
db-test:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: testpass
ports:
- "5433:5432"
tmpfs: /var/lib/postgresql/data
redis-test:
image: redis:7-alpine
ports:
- "6380:6379"
app-test:
build:
context: .
dockerfile: Dockerfile.test
environment:
DATABASE_URL: postgres://test:testpass@db-test:5432/testdb
REDIS_URL: redis://redis-test:6379
depends_on:
- db-test
- redis-test
command: ["npm", "run", "test:integration"]
Здесь мы используем tmpfs для базы данных, чтобы данные не сохранялись между запусками, что ускоряет тесты.
3. Продвинутая настройка TypeScript-тестов с TestContainers
Библиотека testcontainers позволяет управлять контейнерами прямо из кода TypeScript. Это даёт гибкость: вы можете поднимать контейнеры только для определённых тестовых сценариев.
Установка:
npm install --save-dev testcontainers
Пример интеграционного теста с динамическим контейнером:
import { GenericContainer, StartedTestContainer } from 'testcontainers';
import { Client } from 'pg';
import { UserService } from './user-service';
let container: StartedTestContainer;
let client: Client;
beforeAll(async () => {
// Запускаем PostgreSQL контейнер
container = await new GenericContainer('postgres:15-alpine')
.withEnvironment({
POSTGRES_DB: 'test',
POSTGRES_USER: 'test',
POSTGRES_PASSWORD: 'test'
})
.withExposedPorts(5432)
.start();
// Подключаемся к тестовой БД
const host = container.getHost();
const port = container.getMappedPort(5432);
client = new Client({
host,
port,
database: 'test',
user: 'test',
password: 'test'
});
await client.connect();
// Выполняем миграции
await client.query(`
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255)
)
`);
});
afterAll(async () => {
await client.end();
await container.stop();
});
test('UserService.createUser создаёт пользователя в БД', async () => {
const userService = new UserService(client);
const user = await userService.createUser({
email: 'test@example.com',
name: 'Test User'
});
const result = await client.query('SELECT * FROM users WHERE email = $1', ['test@example.com']);
expect(result.rows[0]).toMatchObject({
email: 'test@example.com',
name: 'Test User'
});
});
Такой подход позволяет писать тесты, которые полностью изолированы и не зависят от внешней инфраструктуры.
4. Параллельный запуск тестов и управление ресурсами
При большом количестве тестов важно оптимизировать использование Docker. Используйте --parallel в Jest и настройте пул контейнеров:
// jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.integration.test.ts'],
maxWorkers: 4,
globalSetup: './test/global-setup.ts',
globalTeardown: './test/global-teardown.ts'
};
Пример глобальной настройки для переиспользования контейнеров:
// test/global-setup.ts
import { GenericContainer } from 'testcontainers';
export default async function setup() {
const dbContainer = await new GenericContainer('postgres:15-alpine')
.withEnvironment({
POSTGRES_DB: 'test',
POSTGRES_USER: 'test',
POSTGRES_PASSWORD: 'test'
})
.withExposedPorts(5432)
.start();
process.env.DATABASE_HOST = dbContainer.getHost();
process.env.DATABASE_PORT = String(dbContainer.getMappedPort(5432));
(global as any).__DB_CONTAINER__ = dbContainer;
}
Этот подход сокращает время выполнения тестов до 50% за счёт повторного использования контейнеров.
5. Интеграция с CI/CD и обработка ошибок
В пайплайнах GitHub Actions или GitLab CI используйте Docker Compose для запуска тестов:
# .github/workflows/test.yml
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15-alpine
env:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgres://test:test@localhost:5432/test
REDIS_URL: redis://localhost:6379
Важно добавить таймауты и повторные попытки для тестов, работающих с сетью:
test('API должен ответить в течение 5 секунд', async () => {
const response = await fetch('