Testing
Patterns for testing database code with swift-testing and swift-dependencies.
Test Suite Setup
Basic Test Suite
@Suite(
.dependencies {
try $0.bootstrapDatabase()
}
)
struct MyTestSuite {}With Controlled Dependencies
@Suite(
.dependency(\.continuousClock, ImmediateClock()),
.dependency(\.date.now, Date(timeIntervalSince1970: 1_234_567_890)),
.dependency(\.uuid, .incrementing),
.dependencies {
try $0.bootstrapDatabase()
try await $0.defaultSyncEngine.sendChanges()
},
.snapshots(record: .failed)
)
struct BaseTestSuite {}Nested Test Suites
extension BaseTestSuite {
@MainActor
struct RemindersDetailsTests {
@Dependency(\.defaultDatabase) var database
@Test func basics() async throws {
// Test implementation
}
}
}Reading from Database
Fetch for Assertions
@Test func testCounter() async throws {
@Dependency(\.defaultDatabase) var database
// Act
try database.write { db in
try Counter.insert { Counter.Draft(count: 5) }.execute(db)
}
// Assert
let counter = try database.read { db in
try Counter.fetchOne(db)!
}
#expect(counter.count == 5)
}Fetch One Record
let remindersList = try await database.read { try RemindersList.fetchOne($0)! }Fetch All Records
let attendees = try database.read { db in
try Attendee.where { $0.syncUpID.eq(syncUp.id) }.fetchAll(db)
}Seeding Test Data
Define Seed Extension
extension Database {
func seed() throws {
try seed {
SyncUp(id: UUID(1), seconds: 60, theme: .appOrange, title: "Design")
SyncUp(id: UUID(2), seconds: 60 * 10, theme: .periwinkle, title: "Engineering")
for name in ["Blob", "Blob Jr", "Blob Sr"] {
Attendee.Draft(name: name, syncUpID: UUID(1))
}
for name in ["Blob", "Blob Jr"] {
Attendee.Draft(name: name, syncUpID: UUID(2))
}
Meeting.Draft(
date: Date().addingTimeInterval(-60 * 60 * 24 * 7),
syncUpID: UUID(1),
transcript: "Meeting notes..."
)
}
}
}Use in Test Suite
@Suite(
.dependencies {
try $0.bootstrapDatabase()
try $0.defaultDatabase.write { db in
try db.seed()
}
$0.uuid = .incrementing
}
)
struct SyncUpFormTests {}Testing with @Fetch
Load and Assert
@Test func testRemindersDetail() async throws {
@Dependency(\.defaultDatabase) var database
let remindersList = try await database.read { try RemindersList.fetchOne($0)! }
let model = RemindersDetailModel(detailType: .remindersList(remindersList))
// Load the @Fetch query
try await model.$reminderRows.load()
// Assert on results
#expect(model.reminderRows.count == 4)
}Snapshot Testing
Inline Snapshots
@Test func testModel() async throws {
let model = RemindersDetailModel(detailType: .remindersList(remindersList))
try await model.$reminderRows.load()
assertInlineSnapshot(of: model.reminderRows, as: .customDump) {
#"""
[
[0]: RemindersDetailModel.Row(
reminder: Reminder(
id: UUID(00000000-0000-0000-0000-000000000004),
title: "Haircut",
dueDate: Date(2009-02-11T23:31:30.000Z),
status: .incomplete
)
)
]
"""#
}
}CustomDumpReflectable for Consistent Output
Handle types that don't dump consistently (like SwiftUI.Color):
extension RemindersList: @retroactive CustomDumpReflectable {
public var customDumpMirror: Mirror {
Mirror(
self,
children: [
"id": id,
"color": Color.HexRepresentation(queryOutput: color).hexValue ?? 0,
"position": position,
"title": title,
],
displayStyle: .struct
)
}
}Testing Writes
Test Insert
@Test func testInsert() async throws {
@Dependency(\.defaultDatabase) var database
try database.write { db in
try Counter.insert { Counter.Draft(count: 42) }.execute(db)
}
let counters = try database.read { db in
try Counter.fetchAll(db)
}
#expect(counters.count == 1)
#expect(counters[0].count == 42)
}Test Update
@Test func testUpdate() async throws {
@Dependency(\.defaultDatabase) var database
let id = UUID()
try database.write { db in
try Counter.insert { Counter.Draft(id: id, count: 0) }.execute(db)
try Counter.find(id).update { $0.count += 1 }.execute(db)
}
let counter = try database.read { db in
try Counter.find(id).fetchOne(db)!
}
#expect(counter.count == 1)
}Test Delete
@Test func testDelete() async throws {
@Dependency(\.defaultDatabase) var database
let id = UUID()
try database.write { db in
try Counter.insert { Counter.Draft(id: id) }.execute(db)
try Counter.find(id).delete().execute(db)
}
let counters = try database.read { db in
try Counter.fetchAll(db)
}
#expect(counters.isEmpty)
}Controlled Dependencies
UUID Incrementing
@Suite(.dependency(\.uuid, .incrementing))
struct MyTests {
@Test func testUUIDs() {
@Dependency(\.uuid) var uuid
#expect(uuid() == UUID(0))
#expect(uuid() == UUID(1))
#expect(uuid() == UUID(2))
}
}Fixed Date
@Suite(.dependency(\.date.now, Date(timeIntervalSince1970: 1_234_567_890)))
struct DateTests {
@Test func testDateComparison() {
@Dependency(\.date.now) var now
// now is always Date(timeIntervalSince1970: 1_234_567_890)
}
}Immediate Clock
@Suite(.dependency(\.continuousClock, ImmediateClock()))
struct TimerTests {
@Test func testTimer() async {
// All sleep operations complete immediately
}
}Best Practices
- Use
@Suite(.dependencies {})to configure test dependencies - Seed data in suite setup for consistent test state
- Use
.incrementingUUID for predictable IDs in tests - Use fixed
date.nowfor time-dependent tests - Use
ImmediateClockto avoid waiting in tests - Load
@Fetchqueries with$property.load()before assertions - Use
assertInlineSnapshotfor comprehensive output validation - Define
CustomDumpReflectablefor types with inconsistent dumps - Bootstrap database in test suite setup
- Test in transactions - each test gets fresh database state