Интеграционное тестирование Swift с Docker: от А до Я

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему 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,                

Блоги

Рекомендаций по книгам