Интеграционное тестирование TypeScript с Docker: от А до Я
Современная разработка на TypeScript требует не только качественного юнит-тестирования, но и проверки взаимодействия компонентов в реальных условиях. Интеграционное тестирование с использованием Docker позволяет поднимать изолированные окружения с базами данных, кэшами и другими сервисами, что делает тесты максимально приближенными к production. В этой статье мы разберем, как настроить интеграционное тестирование TypeScript-приложения с помощью Docker, напишем тесты для REST API и рассмотрим лучшие практики.
Почему Docker для интеграционного тестирования?
Docker решает одну из главных проблем интеграционных тестов — зависимость от внешних сервисов. Вместо того чтобы использовать общую базу данных или Redis, которые могут быть заняты другими разработчиками, мы поднимаем изолированные контейнеры для каждого тестового запуска. Это гарантирует:
- Чистоту тестов: каждый запуск начинается с пустого состояния
- Повторяемость: тесты работают одинаково на любой машине
- Изоляцию: тесты не влияют друг на друга и на production-данные
- Скорость: контейнеры поднимаются за секунды
Настройка проекта и Docker Compose
Создадим простой проект на TypeScript с Express и PostgreSQL. Наша задача — протестировать эндпоинт создания пользователя. Начнем с файла docker-compose.test.yml:
version: '3.8'
services:
db_test:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
interval: 5s
timeout: 5s
retries: 5
app_test:
build:
context: .
dockerfile: Dockerfile.test
environment:
DATABASE_URL: postgres://testuser:testpass@db_test:5432/testdb
NODE_ENV: test
ports:
- "3001:3000"
depends_on:
db_test:
condition: service_healthy
Теперь создадим Dockerfile.test для сборки TypeScript-приложения:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
Написание интеграционных тестов с Jest и Supertest
Установим необходимые пакеты:
npm install --save-dev jest @types/jest ts-jest supertest @types/supertest
Создадим конфигурацию Jest (jest.config.ts):
export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/__tests__/**/*.test.ts'],
setupFilesAfterSetup: ['./jest.setup.ts'],
testTimeout: 30000,
};
Файл jest.setup.ts будет отвечать за запуск и остановку Docker-контейнеров:
import { execSync, spawn } from 'child_process';
import path from 'path';
const composeFile = path.resolve(__dirname, '../docker-compose.test.yml');
beforeAll(async () => {
// Поднимаем контейнеры
execSync(`docker-compose -f ${composeFile} up -d --build`, { stdio: 'inherit' });
// Ждем, пока приложение станет доступным
await new Promise((resolve) => setTimeout(resolve, 5000));
});
afterAll(async () => {
// Останавливаем и удаляем контейнеры
execSync(`docker-compose -f ${composeFile} down -v`, { stdio: 'inherit' });
});
Теперь напишем сам тест. Создадим файл __tests__/users.test.ts:
import request from 'supertest';
const BASE_URL = 'http://localhost:3001/api';
describe('Users API Integration Tests', () => {
const testUser = {
name: 'Иван Иванов',
email: 'ivan@example.com',
password: 'securePass123'
};
it('должен создать нового пользователя', async () => {
const response = await request(BASE_URL)
.post('/users')
.send(testUser)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(testUser.name);
expect(response.body.email).toBe(testUser.email);
});
it('должен получить пользователя по ID', async () => {
// Сначала создаем пользователя
const createRes = await request(BASE_URL)
.post('/users')
.send(testUser);
const userId = createRes.body.id;
// Получаем его по ID
const response = await request(BASE_URL)
.get(`/users/${userId}`)
.expect(200);
expect(response.body.id).toBe(userId);
expect(response.body.email).toBe(testUser.email);
});
it('должен вернуть 404 для несуществующего пользователя', async () => {
await request(BASE_URL)
.get('/users/9999')
.expect(404);
});
it('должен вернуть ошибку при дублировании email', async () => {
await request(BASE_URL)
.post('/users')
.send(testUser)
.expect(201);
await request(BASE_URL)
.post('/users')
.send(testUser)
.expect(409);
});
});
Оптимизация и best practices
Чтобы интеграционные тесты были быстрыми и надежными, следуйте этим рекомендациям:
- Используйте healthcheck'и в Docker Compose: это гарантирует, что сервисы готовы к работе до запуска тестов
- Параллелизируйте тесты: если у вас много тестов, используйте
jest --maxWorkers=4для ускорения - Очищайте данные между тестами: используйте
beforeEachдля удаления записей из базы - Не тестируйте то, что уже покрыто юнит-тестами: интеграционные тесты должны проверять взаимодействие компонентов, а не бизнес-логику
Пример очистки данных между тестами:
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
beforeEach(async () => {
await pool.query('TRUNCATE TABLE users CASCADE');
});
Запуск тестов в CI/CD
Для запуска в GitHub Actions используйте следующий workflow:
name: Integration Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses