Интеграционное тестирование TypeScript с Docker: продвинутый уровень
Интеграционное тестирование — ключевой этап в разработке современных TypeScript-приложений. Когда ваша система состоит из множества микросервисов, баз данных, очередей сообщений и внешних API, изолированные unit-тесты перестают покрывать реальные сценарии взаимодействия. Docker позволяет создавать воспроизводимые, изолированные среды для тестирования, приближенные к production. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования TypeScript-приложений с использованием Docker: от настройки контейнеров до построения CI/CD пайплайнов.
Почему Docker — идеальный инструмент для интеграционных тестов TypeScript
Традиционные подходы к интеграционному тестированию часто страдают от проблем с окружением: "на моей машине работает". Docker решает эту проблему, предоставляя:
- Изоляцию — каждый тест запускается в чистом контейнере, без влияния на другие процессы
- Воспроизводимость — одинаковое окружение на всех этапах: локально, в CI, на staging
- Скорость — легковесные контейнеры запускаются за секунды
- Гибкость — легко комбинировать разные версии БД, кэшей, очередей
Для TypeScript-проектов Docker особенно полезен, так как позволяет тестировать не только Node.js рантайм, но и взаимодействие с TypeScript-компилятором, транспиляцией и сборкой.
Настройка Docker-окружения для TypeScript-тестов
Начнем с базовой конфигурации. Создадим Dockerfile для продакшн-сборки и отдельный для тестов:
# Dockerfile.test
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
# Сборка TypeScript
RUN npm run build
# Этап для тестов
FROM node:18-alpine AS test-runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
COPY tests/ ./tests/
COPY jest.config.ts ./
# Запуск интеграционных тестов
CMD ["npx", "jest", "--config", "jest.config.ts", "--testMatch", "**/*.integration.test.ts"]
Для управления несколькими сервисами используем docker-compose.yml:
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
- DB_HOST=postgres
- DB_PORT=5432
- REDIS_HOST=redis
- REDIS_PORT=6379
volumes:
- ./tests:/app/tests:ro
networks:
- test-network
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
interval: 5s
timeout: 5s
retries: 5
networks:
- test-network
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
networks:
- test-network
networks:
test-network:
driver: bridge
Написание продвинутых интеграционных тестов на TypeScript
Теперь создадим тест, который проверяет полный сценарий: запись в БД, кэширование в Redis, и отправку события в очередь. Используем библиотеку testcontainers для динамического управления контейнерами из TypeScript-кода:
// tests/integration/user-registration.integration.test.ts
import { GenericContainer, StartedTestContainer, Wait } from 'testcontainers';
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { RedisContainer } from '@testcontainers/redis';
import axios from 'axios';
describe('User Registration Integration', () => {
let postgresContainer: StartedTestContainer;
let redisContainer: StartedTestContainer;
let appContainer: StartedTestContainer;
beforeAll(async () => {
// Запускаем PostgreSQL
postgresContainer = await new PostgreSqlContainer()
.withDatabase('testdb')
.withUsername('testuser')
.withPassword('testpass')
.start();
// Запускаем Redis
redisContainer = await new RedisContainer()
.start();
// Собираем и запускаем наше приложение
appContainer = await GenericContainer
.fromDockerfile('./')
.withBuildArgs({
BUILD_MODE: 'test'
})
.build();
// Запускаем с переменными окружения
appContainer = await appContainer
.withEnvironment({
DB_HOST: postgresContainer.getHost(),
DB_PORT: String(postgresContainer.getMappedPort(5432)),
REDIS_HOST: redisContainer.getHost(),
REDIS_PORT: String(redisContainer.getMappedPort(6379)),
NODE_ENV: 'test'
})
.withExposedPorts(3000)
.withWaitStrategy(Wait.forHttp('/', 3000))
.start();
});
afterAll(async () => {
await appContainer?.stop();
await redisContainer?.stop();
await postgresContainer?.stop();
});
it('должен успешно зарегистрировать пользователя и сохранить в БД', async () => {
const appUrl = `http://localhost:${appContainer.getMappedPort(3000)}`;
const response = await axios.post(`${appUrl}/api/users/register`, {
email: 'test@example.com',
password: 'StrongP@ss1',
name: 'Test User'
});
expect(response.status).toBe(201);
expect(response.data).toHaveProperty('id');
expect(response.data.email).toBe('test@example.com');
// Проверяем, что пользователь сохранен в БД
const dbResponse = await axios.get(
`${appUrl}/api/users/${response.data.id}`
);
expect(dbResponse.data.name).toBe('Test User');
});
it('должен кэшировать данные в Redis после регистрации', async () => {
const appUrl = `http://localhost:${appContainer.getMappedPort(3000)}`;
// Регистрируем пользователя
const registerRes = await axios.post(`${appUrl}/api/users/register`, {
email: 'cache_test@example.com',
password: 'AnotherP@ss1'
});
// Немедленный запрос должен вернуть данные из кэша
const cachedRes = await axios.get(
`${appUrl}/api/users/${registerRes.data.id}`
);
expect(cachedRes.headers['x-cache']).toBe('HIT');