Ephemeral PostgreSQL databases for Swift Testing
Find a file
2026-04-11 01:42:00 +02:00
Sources feat: restore pooled databases to a clean baseline on retain 2026-04-11 01:42:00 +02:00
Tests/DatabaseTestingTests feat: restore pooled databases to a clean baseline on retain 2026-04-11 01:42:00 +02:00
.gitignore Initial Commit 2026-04-09 03:56:31 +02:00
.swiftformat refactor 2026-04-10 01:39:22 +02:00
AGENTS.md add agents md 2026-04-10 01:42:28 +02:00
Package.resolved refactor 2026-04-10 01:39:22 +02:00
Package.swift chore: update min platform requirement 2026-04-10 12:27:05 +02:00
README.md feat: restore pooled databases to a clean baseline on retain 2026-04-11 01:42:00 +02:00

DatabaseTesting

Ephemeral PostgreSQL databases for Swift Testing. Spins up Docker containers on demand, pools them for concurrent test execution, and tears them down when the suite finishes.

Requirements

  • Swift 6.3+ / swift-tools-version 6.3
  • macOS 26+
  • Docker (accessible at /usr/local/bin/docker)

Installation

Add the package to your Package.swift:

dependencies: [
    .package(url: "https://github.com/noahkamara/swift-database-testing", from: "0.1.0"),
]

Then add the module you need to your test target:

.testTarget(
    name: "MyTests",
    dependencies: [
        .product(name: "DatabaseTesting", package: "swift-database-testing"),
    ]
)

Usage

Apply the .database() trait to a suite or test. The trait manages the pool lifecycle automatically -- containers are created as needed, reset to a clean baseline every time they are retained, and destroyed when the outermost suite finishes.

import Testing
import DatabaseTesting

@Suite(.database())
struct MyDatabaseTests {
    @Test func insertAndQuery() async throws {
        let db = try TestDatabase.current()

        // db.host, db.port, db.username, db.password, db.databaseName
        // are all available to connect your client of choice.
    }
}

Preparing the database

Pass a prepare closure to run setup (migrations, seed data, etc.) against each database after the pool restores its clean baseline for the current test scope:

@Suite(.database(prepare: { db in
    // Run migrations, create tables, insert seed data.
}))
struct MigrationTests {
    @Test func readsSeedData() async throws {
        let db = try TestDatabase.current()
        // ...
    }
}

Modules

Module Purpose
DatabaseTestingCore Docker container lifecycle, TestDatabase, DatabasePool, retry helpers. No dependency on Swift Testing.
DatabaseTesting Swift Testing integration: DatabaseTrait, TaskLocal context, .database() trait API.

Import DatabaseTestingCore directly if you want the pool and container management without the Swift Testing trait (e.g., for XCTest or custom harnesses).

Configuration

The pool reads its configuration from environment variables at first use:

Variable Default Description
TEST_DB_CAPACITY 8 Maximum number of concurrent database containers.
TEST_DB_IMAGE postgres:17-alpine Docker image to use for PostgreSQL.

How it works

  1. The first time a test needs a database, PoolRegistry initializes. It looks for existing containers labeled testdb.managed=true, rebuilds their clean baselines, and reclaims them so a crashed previous run doesn't leak containers.
  2. When a test retains a database and none are available, a new container is launched up to the configured capacity, initialized with a canonical clean baseline, and restored before it is handed out. If the pool is full, the test waits (with a 10-second timeout).
  3. User-provided .database(prepare:) closures are stacked by suite/test scope and applied in order after the pool restores that clean baseline.
  4. Containers use tmpfs-backed PGDATA for faster writes and ephemeral storage.
  5. Each container gets a stable indexed name (testdb_<index>), user, database name, and companion baseline database. The host port is dynamically assigned (-p 0:5432).
  6. When the outermost DatabaseTrait-scoped suite finishes, all containers are destroyed in parallel.

License

See LICENSE for details.