Integration Testing Swift with Docker: Advanced Level
Integration testing is a key stage in developing reliable Swift applications. Unlike unit tests, it verifies the interaction between real components: database, network services, file system. Docker allows isolating these dependencies, making tests reproducible and independent of the developer's environment. In this article, we will explore advanced techniques for integration testing Swift applications (iOS and server-side) using Docker.
1. Basics of Docker Containerization for Swift Tests
Before diving into advanced scenarios, it is important to understand the basic architecture. Docker allows running dependencies (PostgreSQL, Redis, mock servers) in isolated containers that are automatically started before tests and destroyed after.
For server-side Swift (Vapor, Kitura), we use the official Swift image. For iOS applications, Docker cannot run a simulator, but it is excellent for testing the network layer and backend.
Example of a basic Docker Compose file for a Vapor project:
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"This file starts three containers: the Swift server itself, PostgreSQL, and Redis. Now let's write an integration test that checks saving a user to a real database.
2. Writing Integration Tests with XCTest and Docker
For integration testing of server-side Swift, we will use the standard framework XCTest. The key point is that tests must run inside a container that has access to other containers.
Example test for a Vapor application:
import XCTest@testable import Appimport Vapor
final class UserIntegrationTests: XCTestCase {
var app: Application!
override func setUp() async throws { app = try await Application.make(.testing) try await configure(app)
// Database migration before each test try await app.autoMigrate() }
override func tearDown() async throws { try await app.autoRevert() try await app.asyncShutdown() }
func testCreateAndFetchUser() async throws { // Create a user via 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") })
// Check that the user was saved in the database 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") }) }}This test starts the application, makes HTTP requests to the real server inside the container, and checks the responses. Docker ensures that the database and Redis are clean for each run.
3. Mock Servers and Isolation of External Dependencies
At an advanced level, it is often necessary to test interaction with external APIs (payment systems, weather services)