Введение: почему Docker и Swift — идеальная пара для интеграционного тестирования?
Современная разработка под iOS и macOS требует не только юнит-тестов, но и полноценного интеграционного тестирования. Когда ваше приложение взаимодействует с базой данных, внешними API или файловой системой, изолированное тестирование каждого компонента перестаёт быть достаточным. Именно здесь на помощь приходит Docker.
Docker позволяет создавать идентичные окружения для тестов, устраняя проблему «на моей машине работает». Для Swift-разработчиков это особенно актуально: контейнеризация даёт возможность тестировать серверные Swift-приложения (Vapor, Kitura), а также эмулировать инфраструктуру для iOS-приложений (базы данных, кэши, очереди сообщений).
В этой статье мы разберём полный цикл настройки интеграционного тестирования Swift с Docker: от создания тестового окружения до запуска в CI/CD.
1. Подготовка окружения: Docker и Swift
1.1. Установка Docker и Swift Toolchain
Прежде всего убедитесь, что у вас установлены:
- Docker Desktop (для macOS) или Docker Engine (для Linux)
- Swift Toolchain версии 5.7+ (скачать можно с swift.org)
- Xcode 14+ (если тестируете iOS-компоненты)
1.2. Создание Dockerfile для Swift
Начнём с создания базового Docker-образа для нашего Swift-приложения. Создайте файл Dockerfile в корне проекта:
# Dockerfile
FROM swift:5.9-focal AS base
# Устанавливаем рабочую директорию
WORKDIR /app
# Копируем файлы манифеста Package.swift
COPY Package.swift ./
COPY Sources/ ./Sources/
COPY Tests/ ./Tests/
# Сборка проекта
RUN swift build -c debug
# Финальный образ для тестов
FROM base AS test
CMD ["swift", "test", "--parallel"]
Этот образ собирает проект и запускает тесты. Для интеграционных тестов нам понадобятся дополнительные сервисы.
2. Интеграционное тестирование с Docker Compose
Docker Compose позволяет запускать несколько контейнеров одновременно. Для типичного сценария нам понадобятся:
- Контейнер с нашим Swift-приложением
- Контейнер с PostgreSQL (или другой БД)
- Контейнер с Redis (для кэширования)
2.1. Пример docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
POSTGRES_DB: test_db
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U test_user -d test_db"]
interval: 5s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
swift-tests:
build:
context: .
dockerfile: Dockerfile.test
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
environment:
DATABASE_URL: "postgres://test_user:test_pass@postgres:5432/test_db"
REDIS_URL: "redis://redis:6379"
volumes:
- .:/app
2.2. Специальный Dockerfile для тестов
Создадим Dockerfile.test, который будет ждать готовности зависимостей:
# Dockerfile.test
FROM swift:5.9-focal
WORKDIR /app
# Устанавливаем netcat для проверки соединений
RUN apt-get update && apt-get install -y netcat-openbsd && rm -rf /var/lib/apt/lists/*
COPY Package.swift ./
RUN swift package resolve
COPY . .
# Ждём PostgreSQL и Redis
COPY wait-for-services.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/wait-for-services.sh
CMD ["wait-for-services.sh"]
Скрипт wait-for-services.sh:
#!/bin/bash
# wait-for-services.sh
echo "Waiting for PostgreSQL..."
while ! nc -z postgres 5432; do
sleep 1
done
echo "PostgreSQL is ready!"
echo "Waiting for Redis..."
while ! nc -z redis 6379; do
sleep 1
done
echo "Redis is ready!"
echo "Running tests..."
swift test --parallel
3. Написание интеграционных тестов на Swift
Теперь напишем сами тесты, используя XCTest и библиотеку для работы с базой данных (например, PostgresClientKit или Vapor).
3.1. Тест для проверки подключения к БД
// Tests/IntegrationTests/DatabaseTests.swift
import XCTest
@testable import YourAppModule
final class DatabaseIntegrationTests: XCTestCase {
var dbConnection: DatabaseConnection!
override func setUp() async throws {
// Используем переменные окружения, переданные из Docker
let databaseURL = ProcessInfo.processInfo.environment["DATABASE_URL"]!
dbConnection = try await DatabaseConnection(url: databaseURL)
}
override func tearDown() async throws {
try await dbConnection.close()
}
func testDatabaseConnection() async throws {
// Проверяем, что можем выполнить простой запрос
let result = try await dbConnection.query("SELECT 1")
XCTAssertEqual(result, 1, "База данных должна отвечать на SELECT 1")
}
func testUserCreation() async throws {
// Создаём тестового пользователя
let user = User(name: "Test User", email: "test@example.com")
let savedUser = try await dbConnection.save(user)
// Проверяем, что пользователь сохранён
XCTAssertNotNil(savedUser.id, "ID пользователя должен быть присвоен")
// Очищаем тестовые данные
try await dbConnection.delete(savedUser)
}
}
3.2. Тест для проверки кэширования Redis
// Tests/IntegrationTests/CacheTests.swift
import XCTest
@testable import YourAppModule
final class CacheIntegrationTests: XCTestCase {
var cache: RedisCache!
override func setUp() async throws {
let redisURL = ProcessInfo.processInfo.environment["REDIS_URL"]!
cache = try await RedisCache(url: redisURL)
}
override func tearDown() async throws {
try await cache.flushAll()
try await cache.close()
}
func testSetAndGet() async throws {
// Устанавливаем значение в кэш
try await cache.set(key: "test_key", value: "test_value")
// Получаем значение
let value = try await cache.get(key: "test_key")
XCTAssertEqual(value,