Swift Testing
Overview
This skill provides expert guidance on Swift Testing, covering the modern Swift Testing framework, test doubles (mocks, stubs, spies), fixtures, integration testing, snapshot testing, and migration from XCTest. Use this skill to help developers write reliable, maintainable tests following F.I.R.S.T. principles and Arrange-Act-Assert patterns.
Agent Behavior Contract (Follow These Rules)
- Use Swift Testing framework (
@Test, #expect, #require, @Suite) for all new tests, not XCTest.
- Always structure tests with clear Arrange-Act-Assert phases.
- Follow F.I.R.S.T. principles: Fast, Isolated, Repeatable, Self-Validating, Timely.
- Use proper test double terminology per Martin Fowler's taxonomy (Dummy, Fake, Stub, Spy, SpyingStub, Mock).
- Place fixtures close to models with
#if DEBUG, not in test targets.
- Place test doubles close to interfaces with
#if DEBUG, not in test targets.
- Prefer state verification over behavior verification - simpler, less brittle tests.
- Use
#expect for soft assertions (continue on failure) and #require for hard assertions (stop on failure).
Quick Decision Tree
When a developer needs testing guidance, follow this decision tree:
Starting fresh with Swift Testing?
- Read
references/test-organization.md for suites, tags, traits
- Read
references/async-testing.md for async test patterns
Need to create test data?
- Read
references/fixtures.md for fixture patterns and placement
- Read
references/test-doubles.md for mock/stub/spy patterns
Testing multiple inputs?
- Read
references/parameterized-tests.md for parameterized testing
Testing module interactions?
- Read
references/integration-testing.md for integration test patterns
Testing UI for regressions?
- Read
references/snapshot-testing.md for snapshot testing setup
Testing data structures or state?
- Read
references/dump-snapshot-testing.md for text-based snapshot testing
Migrating from XCTest?
- Read
references/migration-xctest.md for migration guide
Triage-First Playbook (Common Errors -> Next Best Move)
- "XCTAssertEqual is unavailable" / need to modernize tests
- Use
references/migration-xctest.md for XCTest to Swift Testing migration
- Need to test async code
- Use
references/async-testing.md for async patterns, confirmation, timeouts
- Tests are slow or flaky
- Check F.I.R.S.T. principles, use proper mocking per
references/test-doubles.md
- Need deterministic test data
- Use
references/fixtures.md for fixture patterns with fixed dates
- Need to test multiple scenarios efficiently
- Use
references/parameterized-tests.md for parameterized testing
- Need to verify component interactions
- Use
references/integration-testing.md for integration test patterns
Core Syntax
Basic Test
import Testing
@Test func basicTest() {
#expect(1 + 1 == 2)
}
Test with Description
@Test("Adding items increases cart count")
func addItem() {
let cart = Cart()
cart.add(item)
#expect(cart.count == 1)
}
Async Test
@Test func asyncOperation() async throws {
let result = try await service.fetch()
#expect(result.isValid)
}
Arrange-Act-Assert Pattern
Structure every test with clear phases:
@Test func calculateTotal() {
// Given
let cart = ShoppingCart()
cart.add(Item(price: 10))
cart.add(Item(price: 20))
// When
let total = cart.calculateTotal()
// Then
#expect(total == 30)
}
Assertions
#expect - Soft Assertion
Continues test execution after failure:
@Test func multipleExpectations() {
let user = User(name: "Alice", age: 30)
#expect(user.name == "Alice") // If fails, test continues
#expect(user.age == 30) // This still runs
}
#require - Hard Assertion
Stops test execution on failure:
@Test func requireExample() throws {
let user = try #require(fetchUser()) // Stops if nil
#expect(user.name == "Alice")
}
Error Testing
@Test func throwsError() {
#expect(throws: ValidationError.self) {
try validate(invalidInput)
}
}
@Test func throwsSpecificError() {
#expect(throws: ValidationError.emptyField) {
try validate("")
}
}
F.I.R.S.T. Principles
| Principle |
Description |
Application |
| Fast |
Tests execute in milliseconds |
Mock expensive operations |
| Isolated |
Tests don't depend on each other |
Fresh instance per test |
| Repeatable |
Same result every time |
Mock dates, network, external deps |
| Self-Validating |
Auto-report pass/fail |
Use #expect, never rely on print() |
| Timely |
Write tests alongside code |
Use parameterized tests for edge cases |
Test Double Quick Reference
Per Martin Fowler's definition:
| Type |
Purpose |
Verification |
| Dummy |
Fill parameters, never used |
N/A |
| Fake |
Working implementation with shortcuts |
State |
| Stub |
Provides canned answers |
State |
| Spy |
Records calls for verification |
State |
| SpyingStub |
Stub + Spy combined (most common) |
State |
| Mock |
Pre-programmed expectations, self-verifies |
Behavior |
Important: What Swift community calls "Mock" is usually a SpyingStub.
For detailed patterns, see references/test-doubles.md.
Test Double Placement
Place test doubles close to the interface, not in test targets:
// In PersonalRecordsCore-Interface/Sources/...
public protocol PersonalRecordsRepositoryProtocol: Sendable {
func getAll() async throws -> [PersonalRecord]
func save(_ record: PersonalRecord) async throws
}
#if DEBUG
public final class PersonalRecordsRepositorySpyingStub: PersonalRecordsRepositoryProtocol {
// Spy: Captured calls
public private(set) var savedRecords: [PersonalRecord] = []
// Stub: Configurable responses
public var recordsToReturn: [PersonalRecord] = []
public var errorToThrow: Error?
public func getAll() async throws -> [PersonalRecord] {
if let error = errorToThrow { throw error }
return recordsToReturn
}
public func save(_ record: PersonalRecord) async throws {
if let error = errorToThrow { throw error }
savedRecords.append(record)
}
}
#endif
Fixtures
Place fixtures close to the model:
// In Sources/Models/PersonalRecord.swift
public struct PersonalRecord: Equatable, Sendable {
public let id: UUID
public let weight: Double
// ...
}
#if DEBUG
extension PersonalRecord {
public static func fixture(
id: UUID = UUID(),
weight: Double = 100.0
// ... defaults for all properties
) -> PersonalRecord {
PersonalRecord(id: id, weight: weight)
}
}
#endif
For detailed patterns, see references/fixtures.md.
Test Pyramid
+-------------+
| UI Tests | 5% - End-to-end flows
| (E2E) |
+-------------+
| Integration | 15% - Module interactions
| Tests |
+-------------+
| Unit | 80% - Individual components
| Tests |
+-------------+
Reference Files
Load these files as needed for specific topics:
test-organization.md - Suites, tags, traits, parallel execution
parameterized-tests.md - Testing multiple inputs efficiently
async-testing.md - Async patterns, confirmation, timeouts, cancellation
migration-xctest.md - Complete XCTest to Swift Testing migration guide
test-doubles.md - Complete taxonomy with examples (Dummy, Fake, Stub, Spy, SpyingStub, Mock)
fixtures.md - Fixture patterns, placement, and best practices
integration-testing.md - Module interaction testing patterns
snapshot-testing.md - UI regression testing with SnapshotTesting library
dump-snapshot-testing.md - Text-based snapshot testing for data structures
Best Practices Summary
- Use Swift Testing for new tests - Modern syntax, better features
- Follow Arrange-Act-Assert - Clear test structure
- Apply F.I.R.S.T. principles - Fast, Isolated, Repeatable, Self-Validating, Timely
- Place fixtures near models - With
#if DEBUG guards
- Place test doubles near interfaces - With
#if DEBUG guards
- Prefer state verification - Simpler, less brittle than behavior verification
- Use parameterized tests - For testing multiple inputs efficiently
- Follow test pyramid - 80% unit, 15% integration, 5% UI
Verification Checklist (When You Write Tests)
- Tests follow Arrange-Act-Assert pattern
- Test names describe behavior, not implementation
- Fixtures use sensible defaults, not random values
- Test doubles are minimal (only stub what's needed)
- Async tests use proper patterns (async/await, confirmation)
- Tests are fast (mock expensive operations)
- Tests are isolated (no shared state)
- Tests are repeatable (no flaky date/time dependencies)
1---2name: swift-testing3description: Expert guidance on Swift Testing best practices, patterns, and implementation. Use when developers mention: (1) Swift Testing, @Test, #expect, #require, or @Suite, (2) "use Swift Testing" or "modern testing patterns", (3) test doubles, mocks, stubs, spies, or fixtures, (4) unit tests, integration tests, or snapshot tests, (5) migrating from XCTest to Swift Testing, (6) TDD, Arrange-Act-Assert, or F.I.R.S.T. principles, (7) parameterized tests or test organization.4---5# Swift Testing
6
7## Overview
8
9This skill provides expert guidance on Swift Testing, covering the modern Swift Testing framework, test doubles (mocks, stubs, spies), fixtures, integration testing, snapshot testing, and migration from XCTest. Use this skill to help developers write reliable, maintainable tests following F.I.R.S.T. principles and Arrange-Act-Assert patterns.
10
11## Agent Behavior Contract (Follow These Rules)
12
131. Use Swift Testing framework (`@Test`, `#expect`, `#require`, `@Suite`) for all new tests, not XCTest.
142. Always structure tests with clear Arrange-Act-Assert phases.
153. Follow F.I.R.S.T. principles: Fast, Isolated, Repeatable, Self-Validating, Timely.
164. Use proper test double terminology per Martin Fowler's taxonomy (Dummy, Fake, Stub, Spy, SpyingStub, Mock).
175. Place fixtures close to models with `#if DEBUG`, not in test targets.
186. Place test doubles close to interfaces with `#if DEBUG`, not in test targets.
197. Prefer state verification over behavior verification - simpler, less brittle tests.
208. Use `#expect` for soft assertions (continue on failure) and `#require` for hard assertions (stop on failure).
21
22## Quick Decision Tree
23
24When a developer needs testing guidance, follow this decision tree:
25
261. **Starting fresh with Swift Testing?**
27 - Read `references/test-organization.md` for suites, tags, traits
28 - Read `references/async-testing.md` for async test patterns
29
302. **Need to create test data?**
31 - Read `references/fixtures.md` for fixture patterns and placement
32 - Read `references/test-doubles.md` for mock/stub/spy patterns
33
343. **Testing multiple inputs?**
35 - Read `references/parameterized-tests.md` for parameterized testing
36
374. **Testing module interactions?**
38 - Read `references/integration-testing.md` for integration test patterns
39
405. **Testing UI for regressions?**
41 - Read `references/snapshot-testing.md` for snapshot testing setup
42
436. **Testing data structures or state?**
44 - Read `references/dump-snapshot-testing.md` for text-based snapshot testing
45
467. **Migrating from XCTest?**
47 - Read `references/migration-xctest.md` for migration guide
48
49## Triage-First Playbook (Common Errors -> Next Best Move)
50
51- "XCTAssertEqual is unavailable" / need to modernize tests
52 - Use `references/migration-xctest.md` for XCTest to Swift Testing migration
53- Need to test async code
54 - Use `references/async-testing.md` for async patterns, confirmation, timeouts
55- Tests are slow or flaky
56 - Check F.I.R.S.T. principles, use proper mocking per `references/test-doubles.md`
57- Need deterministic test data
58 - Use `references/fixtures.md` for fixture patterns with fixed dates
59- Need to test multiple scenarios efficiently
60 - Use `references/parameterized-tests.md` for parameterized testing
61- Need to verify component interactions
62 - Use `references/integration-testing.md` for integration test patterns
63
64## Core Syntax
65
66### Basic Test
67
68```swift
69import Testing
70
71@Test func basicTest() {
72 #expect(1 + 1 == 2)
73}
74```
75
76### Test with Description
77
78```swift
79@Test("Adding items increases cart count")
80func addItem() {
81 let cart = Cart()
82 cart.add(item)
83 #expect(cart.count == 1)
84}
85```
86
87### Async Test
88
89```swift
90@Test func asyncOperation() async throws {
91 let result = try await service.fetch()
92 #expect(result.isValid)
93}
94```
95
96## Arrange-Act-Assert Pattern
97
98Structure every test with clear phases:
99
100```swift
101@Test func calculateTotal() {
102 // Given
103 let cart = ShoppingCart()
104 cart.add(Item(price: 10))
105 cart.add(Item(price: 20))
106
107 // When
108 let total = cart.calculateTotal()
109
110 // Then
111 #expect(total == 30)
112}
113```
114
115## Assertions
116
117### #expect - Soft Assertion
118
119Continues test execution after failure:
120
121```swift
122@Test func multipleExpectations() {
123 let user = User(name: "Alice", age: 30)
124 #expect(user.name == "Alice") // If fails, test continues
125 #expect(user.age == 30) // This still runs
126}
127```
128
129### #require - Hard Assertion
130
131Stops test execution on failure:
132
133```swift
134@Test func requireExample() throws {
135 let user = try #require(fetchUser()) // Stops if nil
136 #expect(user.name == "Alice")
137}
138```
139
140### Error Testing
141
142```swift
143@Test func throwsError() {
144 #expect(throws: ValidationError.self) {
145 try validate(invalidInput)
146 }
147}
148
149@Test func throwsSpecificError() {
150 #expect(throws: ValidationError.emptyField) {
151 try validate("")
152 }
153}
154```
155
156## F.I.R.S.T. Principles
157
158| Principle | Description | Application |
159|-----------|-------------|-------------|
160| **Fast** | Tests execute in milliseconds | Mock expensive operations |
161| **Isolated** | Tests don't depend on each other | Fresh instance per test |
162| **Repeatable** | Same result every time | Mock dates, network, external deps |
163| **Self-Validating** | Auto-report pass/fail | Use `#expect`, never rely on `print()` |
164| **Timely** | Write tests alongside code | Use parameterized tests for edge cases |
165
166## Test Double Quick Reference
167
168Per [Martin Fowler's definition](https://martinfowler.com/articles/mocksArentStubs.html):
169
170| Type | Purpose | Verification |
171|------|---------|--------------|
172| **Dummy** | Fill parameters, never used | N/A |
173| **Fake** | Working implementation with shortcuts | State |
174| **Stub** | Provides canned answers | State |
175| **Spy** | Records calls for verification | State |
176| **SpyingStub** | Stub + Spy combined (most common) | State |
177| **Mock** | Pre-programmed expectations, self-verifies | Behavior |
178
179**Important**: What Swift community calls "Mock" is usually a **SpyingStub**.
180
181For detailed patterns, see `references/test-doubles.md`.
182
183## Test Double Placement
184
185Place test doubles **close to the interface**, not in test targets:
186
187```swift
188// In PersonalRecordsCore-Interface/Sources/...
189
190public protocol PersonalRecordsRepositoryProtocol: Sendable {
191 func getAll() async throws -> [PersonalRecord]
192 func save(_ record: PersonalRecord) async throws
193}
194
195#if DEBUG
196public final class PersonalRecordsRepositorySpyingStub: PersonalRecordsRepositoryProtocol {
197 // Spy: Captured calls
198 public private(set) var savedRecords: [PersonalRecord] = []
199
200 // Stub: Configurable responses
201 public var recordsToReturn: [PersonalRecord] = []
202 public var errorToThrow: Error?
203
204 public func getAll() async throws -> [PersonalRecord] {
205 if let error = errorToThrow { throw error }
206 return recordsToReturn
207 }
208
209 public func save(_ record: PersonalRecord) async throws {
210 if let error = errorToThrow { throw error }
211 savedRecords.append(record)
212 }
213}
214#endif
215```
216
217## Fixtures
218
219Place fixtures **close to the model**:
220
221```swift
222// In Sources/Models/PersonalRecord.swift
223
224public struct PersonalRecord: Equatable, Sendable {
225 public let id: UUID
226 public let weight: Double
227 // ...
228}
229
230#if DEBUG
231extension PersonalRecord {
232 public static func fixture(
233 id: UUID = UUID(),
234 weight: Double = 100.0
235 // ... defaults for all properties
236 ) -> PersonalRecord {
237 PersonalRecord(id: id, weight: weight)
238 }
239}
240#endif
241```
242
243For detailed patterns, see `references/fixtures.md`.
244
245## Test Pyramid
246
247```
248 +-------------+
249 | UI Tests | 5% - End-to-end flows
250 | (E2E) |
251 +-------------+
252 | Integration | 15% - Module interactions
253 | Tests |
254 +-------------+
255 | Unit | 80% - Individual components
256 | Tests |
257 +-------------+
258```
259
260## Reference Files
261
262Load these files as needed for specific topics:
263
264- **`test-organization.md`** - Suites, tags, traits, parallel execution
265- **`parameterized-tests.md`** - Testing multiple inputs efficiently
266- **`async-testing.md`** - Async patterns, confirmation, timeouts, cancellation
267- **`migration-xctest.md`** - Complete XCTest to Swift Testing migration guide
268- **`test-doubles.md`** - Complete taxonomy with examples (Dummy, Fake, Stub, Spy, SpyingStub, Mock)
269- **`fixtures.md`** - Fixture patterns, placement, and best practices
270- **`integration-testing.md`** - Module interaction testing patterns
271- **`snapshot-testing.md`** - UI regression testing with SnapshotTesting library
272- **`dump-snapshot-testing.md`** - Text-based snapshot testing for data structures
273
274## Best Practices Summary
275
2761. **Use Swift Testing for new tests** - Modern syntax, better features
2772. **Follow Arrange-Act-Assert** - Clear test structure
2783. **Apply F.I.R.S.T. principles** - Fast, Isolated, Repeatable, Self-Validating, Timely
2794. **Place fixtures near models** - With `#if DEBUG` guards
2805. **Place test doubles near interfaces** - With `#if DEBUG` guards
2816. **Prefer state verification** - Simpler, less brittle than behavior verification
2827. **Use parameterized tests** - For testing multiple inputs efficiently
2838. **Follow test pyramid** - 80% unit, 15% integration, 5% UI
284
285## Verification Checklist (When You Write Tests)
286
287- Tests follow Arrange-Act-Assert pattern
288- Test names describe behavior, not implementation
289- Fixtures use sensible defaults, not random values
290- Test doubles are minimal (only stub what's needed)
291- Async tests use proper patterns (async/await, confirmation)
292- Tests are fast (mock expensive operations)
293- Tests are isolated (no shared state)
294- Tests are repeatable (no flaky date/time dependencies)
295