Интеграционное тестирование Swift с Docker: продвинутый уровень
Интеграционное тестирование — ключевой этап разработки надежных Swift-приложений. В отличие от юнит-тестов, оно проверяет взаимодействие между реальными компонентами: базой данных, сетевыми сервисами, файловой системой. Docker позволяет изолировать эти зависимости, делая тесты воспроизводимыми и независимыми от окружения разработчика. В этой статье мы рассмотрим продвинутые техники интеграционного тестирования Swift-приложений (iOS и серверная сторона) с использованием Docker.
1. Основы Docker-контейнеризации для Swift-тестов
Перед тем как углубляться в продвинутые сценарии, важно понимать базовую архитектуру. Docker позволяет запускать зависимости (PostgreSQL, Redis, mock-серверы) в изолированных контейнерах, которые автоматически поднимаются перед тестами и уничтожаются после.
Для серверного Swift (Vapor, Kitura) мы используем официальный образ Swift. Для iOS-приложений Docker не может запустить симулятор, но отлично подходит для тестирования сетевого слоя и бэкенда.
Пример базового Docker Compose файла для Vapor проекта:
version: '3.8'
services:
app:
image: swift:5.10-focal
working_dir: /app
volumes:
- .:/app
depends_on:
- db
- redis
environment:
- DATABASE_URL=postgres://user:pass@db:5432/testdb
- REDIS_URL=redis://redis:6379
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: testdb
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
Этот файл поднимает три контейнера: сам Swift-сервер, PostgreSQL и Redis. Теперь напишем интеграционный тест, который проверяет сохранение пользователя в реальной базе данных.
2. Написание интеграционных тестов с XCTest и Docker
Для интеграционного тестирования серверного Swift мы будем использовать стандартный фреймворк XCTest. Ключевой момент — тесты должны запускаться внутри контейнера, который имеет доступ к другим контейнерам.
Пример теста для Vapor приложения:
import XCTest
@testable import App
import Vapor
final class UserIntegrationTests: XCTestCase {
var app: Application!
override func setUp() async throws {
app = try await Application.make(.testing)
try await configure(app)
// Миграция базы данных перед каждым тестом
try await app.autoMigrate()
}
override func tearDown() async throws {
try await app.autoRevert()
try await app.asyncShutdown()
}
func testCreateAndFetchUser() async throws {
// Создаем пользователя через API
let userDTO = UserDTO(name: "Alice", email: "alice@test.com")
try await app.test(.POST, "/users", beforeRequest: { req in
try req.content.encode(userDTO)
}, afterResponse: { res in
XCTAssertEqual(res.status, .created)
let createdUser = try res.content.decode(User.self)
XCTAssertEqual(createdUser.name, "Alice")
})
// Проверяем, что пользователь сохранился в базе
try await app.test(.GET, "/users/\(aliceID)", afterResponse: { res in
XCTAssertEqual(res.status, .ok)
let fetchedUser = try res.content.decode(User.self)
XCTAssertEqual(fetchedUser.email, "alice@test.com")
})
}
}
Этот тест запускает приложение, выполняет HTTP-запросы к реальному серверу внутри контейнера и проверяет ответы. Docker гарантирует, что база данных и Redis чисты для каждого запуска.
3. Mock-серверы и изоляция внешних зависимостей
На продвинутом уровне часто требуется тестировать взаимодействие с внешними API (платежные системы, погодные сервисы). Вместо того чтобы делать реальные запросы, мы поднимаем mock-сервер в отдельном Docker-контейнере.
Используем WireMock для имитации внешнего API:
// Docker Compose добавляем сервис:
wiremock:
image: wiremock/wiremock:latest
ports:
- "8080:8080"
volumes:
- ./wiremock/stubs:/home/wiremock
command: --global-response-templating
Тест, использующий mock:
func testPaymentService() async throws {
// Настраиваем WireMock на ответ
let stubMapping = """
{
"request": { "method": "POST", "url": "/payments" },
"response": {
"status": 200,
"jsonBody": { "status": "success", "transactionId": "txn_123" }
}
}
"""
let url = URL(string: "http://wiremock:8080/__admin/mappings")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = stubMapping.data(using: .utf8)
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
_ = try await URLSession.shared.data(for: request)
// Теперь тестируем наш сервис
let paymentService = PaymentService(baseURL: "http://wiremock:8080")
let result = try await paymentService.processPayment(amount: 100)
XCTAssertEqual(result.status, "success")
XCTAssertEqual(result.transactionId, "txn_123")
}
Такой подход позволяет тестировать все сценарии: успешные ответы, ошибки, таймауты, не подключаясь к реальным сервисам.
4. Параллельное выполнение тестов и CI/CD интеграция
Одна из главных проблем интеграционных тестов — время выполнения. Docker позволяет легко распараллелить тесты, поднимая несколько изолированных наборов контейнеров.
Стратегия параллельного выполнения:
- Sharding тестов: разделите тесты на группы (по модулям или фичам) и запускайте каждую группу в отдельном Docker Compose проекте
- Использование тегов: маркируйте тесты, которые требуют одной и той же базы данных, чтобы избежать конфликтов
- Docker Compose с профилями: используйте профили для разных наборов зависимостей
Пример Makefile для параллельного запуска:
# Makefile
.PHONY: test-parallel
test-parallel:
@echo "Запуск интеграционных тестов в 4 параллельных потока..."
docker compose up -d db redis wiremock
sleep 5 # Ждем, пока контейнеры будут готовы
swift test --parallel --num-workers