Введение
Интеграционное тестирование — это критически важный этап разработки, который проверяет взаимодействие компонентов вашего Swift-приложения с внешними зависимостями: базами данных, API, файловыми системами и сетевыми сервисами. Однако традиционные подходы к интеграционному тестированию на macOS/iOS часто страдают от "проблемы окружения" — тесты, работающие на машине разработчика, могут падать на CI-сервере из-за различий в версиях библиотек, настройках сети или конфигурации БД.
Docker решает эту проблему кардинально: вы упаковываете все зависимости (PostgreSQL, Redis, Mock-серверы) в изолированные контейнеры, которые запускаются идентично на любой платформе. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования Swift с Docker: от настройки тестового окружения до параллельного запуска и интеграции с CI/CD.
1. Подготовка тестового окружения с Docker Compose
Начнем с создания изолированного окружения для тестов. Вместо того чтобы запускать каждый сервис вручную, используем docker-compose.yml для декларативного описания всех зависимостей.
Пример docker-compose.yml для Swift-проекта
version: '3.8'
services:
postgres_test:
image: postgres:15-alpine
environment:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
POSTGRES_DB: swift_test_db
ports:
- "5433:5432" # Используем нестандартный порт, чтобы не конфликтовать с локальной БД
tmpfs: /var/lib/postgresql/data # Данные в памяти — быстрое очищение
redis_test:
image: redis:7-alpine
ports:
- "6380:6379"
mock_server:
build: ./mock-server # Кастомный mock-сервер на Vapor
ports:
- "8081:8080"
environment:
LOG_LEVEL: debug
Swift-код для динамической конфигурации тестов
Чтобы тесты подключались к контейнерам Docker, а не к локальным сервисам, используем переменные окружения:
import XCTest
final class IntegrationTests: XCTestCase {
var app: XCTestApplication?
override func setUp() async throws {
// Получаем конфигурацию из переменных окружения (устанавливаются в CI или docker-compose)
let dbHost = ProcessInfo.processInfo.environment["DB_HOST"] ?? "localhost"
let dbPort = Int(ProcessInfo.processInfo.environment["DB_PORT"] ?? "5433") ?? 5433
let redisHost = ProcessInfo.processInfo.environment["REDIS_HOST"] ?? "localhost"
let mockHost = ProcessInfo.processInfo.environment["MOCK_HOST"] ?? "http://localhost:8081"
// Конфигурируем тестовое приложение (например, Vapor или Kitura)
let config = AppConfig(
database: DatabaseConfig(host: dbHost, port: dbPort,
username: "test_user", password: "test_pass",
database: "swift_test_db"),
redis: RedisConfig(host: redisHost, port: 6380),
mockServiceURL: mockHost
)
app = try await XCTestApplication.start(config: config)
}
override func tearDown() async throws {
try await app?.shutdown()
}
}
2. Продвинутая настройка: параллельный запуск и изоляция тестов
Одна из главных проблем интеграционных тестов — их время выполнения. Docker позволяет запускать несколько копий тестового окружения параллельно, что значительно ускоряет прогон.
Стратегия изоляции: один контейнер на тестовый класс
Используем docker-compose --project-name для создания уникальных инстансов:
#!/bin/bash
# Запуск тестов с уникальным project-name для параллельного выполнения
TEST_SUITE=$1
PROJECT_NAME="swift_test_${TEST_SUITE}_$(date +%s)"
docker-compose -p $PROJECT_NAME up -d --force-recreate
sleep 5 # Ждем инициализации контейнеров
# Запускаем конкретный тестовый класс
xcodebuild test -scheme MyApp -destination 'platform=macOS' \
-only-testing:MyAppTests/$TEST_SUITE \
TEST_DB_PORT=5433 TEST_REDIS_PORT=6380
# Очистка
docker-compose -p $PROJECT_NAME down -v
Swift-код для параллельного запуска
import XCTest
class UserServiceTests: IntegrationTestCase {
// Каждый тестовый класс получает свои порты через переменные окружения
override class func setUp() {
// Генерируем уникальные порты для изоляции
let basePort = Int.random(in: 5400..<5500)
setenv("DB_PORT", "\(basePort)", 1)
setenv("REDIS_PORT", "\(basePort + 1)", 1)
super.setUp()
}
func testUserCreation() async throws {
let user = try await UserService.create(name: "Alice", email: "alice@test.com")
XCTAssertNotNil(user.id)
}
func testUserFetch() async throws {
let users = try await UserService.fetchAll()
XCTAssertGreaterThan(users.count, 0)
}
}
3. Mock-сервисы и контрактное тестирование
В реальном проекте вы часто зависите от внешних API (платежные системы, погода, соцсети). Вместо того чтобы тестировать против продакшн-серверов, создаем mock-сервисы в Docker, которые эмулируют поведение реальных API.
Создание Mock-сервера на Vapor
// MockServer/main.swift
import Vapor
let app = try Application(.detect())
defer { app.shutdown() }
// Эмуляция API погоды
app.get("weather") { req -> WeatherResponse in
return WeatherResponse(temperature: 22.5, condition: "sunny")
}
// Эмуляция платежного шлюза
app.post("payment") { req -> PaymentResponse in
let payment = try req.content.decode(PaymentRequest.self)
guard payment.amount > 0 else {
throw Abort(.badRequest, reason: "Invalid amount")
}
return PaymentResponse(status: "success", transactionId: UUID().uuidString)
}
try app.run()
Dockerfile для mock-сервера
FROM swift:5.9-jammy as build
WORKDIR /app
COPY . .
RUN swift build -c release
FROM ubuntu:jammy
WORKDIR /app
COPY --from=build /app/.build/release/MockServer .
EXPOSE 8080
CMD ["./MockServer"]
Тест с проверкой контракта
func testPaymentIntegration() async throws {
let mockURL = ProcessInfo.processInfo.environment["MOCK_HOST"] ?? "http://localhost:8081"
let client = HTTPClient(eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: