# Swift Testing Patterns Reference

## Contents
- Basic Tests and Traits
- Expectations and Requirements
- Suite Organization
- Parameterized Tests
- Confirmation and Known Issues
- Tags
- TestScoping and Test Organization
- Mocking and Test Doubles
- Testable Architecture
- Async and Concurrent Tests
- XCTest UI Tests — Page Object Pattern
- Performance Testing
- Snapshot Testing
- Test Attachments
- Exit Testing
- Test File Organization
- Common Mistakes and Review Checklist

## Basic Tests and Traits

```swift
import Testing

@Test("User can update their display name")
func updateDisplayName() {
    var user = User(name: "Alice")
    user.name = "Bob"
    #expect(user.name == "Bob")
}

@Test(.tags(.validation, .email))
func validatesEmailFormat() { /* ... */ }
```

## Expectations and Requirements

```swift
#expect(result == 42)
#expect(name.isEmpty == false)
#expect(items.count > 0, "Items should not be empty")

// Error type checking
#expect(throws: ValidationError.self) {
    try validate(email: "not-an-email")
}

// Specific error matching
#expect {
    try validate(email: "")
} throws: { error in
    guard let err = error as? ValidationError else { return false }
    return err == .empty
}

// #require unwraps or fails the test
let user = try #require(await fetchUser(id: 1))
let first = try #require(items.first)
```

**Rule: Use `#require` when subsequent assertions depend on the value. Use `#expect` for independent checks.**

## Suite Organization

```swift
@Suite("User Authentication")
struct AuthTests {
    let service: AuthService
    let mockRepo: MockUserRepository

    // init() replaces setUp() -- runs before each test
    init() {
        mockRepo = MockUserRepository()
        service = AuthService(repository: mockRepo)
    }

    @Test func loginSucceeds() async throws {
        let user = try await service.login(email: "test@test.com", password: "pass")
        #expect(user.email == "test@test.com")
    }

    @Test func loginFailsWithBadPassword() async {
        #expect(throws: AuthError.invalidCredentials) {
            try await service.login(email: "test@test.com", password: "wrong")
        }
    }
}
```

Suites can nest for logical grouping:

```swift
@Suite("Payments")
struct PaymentTests {
    @Suite("Subscriptions")
    struct SubscriptionTests {
        @Test func renewsAutomatically() { /* ... */ }
    }
    @Suite("One-Time")
    struct OneTimeTests {
        @Test func chargesCorrectAmount() { /* ... */ }
    }
}
```

## Parameterized Tests

```swift
@Test("Email validation", arguments: [
    ("user@example.com", true),
    ("user@", false),
    ("@example.com", false),
    ("", false),
])
func validateEmail(email: String, isValid: Bool) {
    #expect(EmailValidator.isValid(email) == isValid)
}

// From CaseIterable
@Test(arguments: Currency.allCases)
func currencyHasSymbol(currency: Currency) {
    #expect(currency.symbol.isEmpty == false)
}

// Two collections: cartesian product
@Test(arguments: [1, 2, 3], ["a", "b"])
func combinations(number: Int, letter: String) {
    #expect(number > 0)
}

// Use zip for 1:1 pairing
@Test(arguments: zip(["USD", "EUR"], ["$", "€"]))
func currencySymbols(code: String, symbol: String) {
    #expect(Currency(code: code).symbol == symbol)
}
```

Each argument combination runs as an independent test case reported separately.

## Confirmation and Known Issues

### Confirmation (Async Event Testing)

```swift
// Basic confirmation -- event must fire exactly once
await confirmation("Received notification") { confirm in
    let observer = NotificationCenter.default.addObserver(
        forName: .userLoggedIn, object: nil, queue: .main
    ) { _ in confirm() }
    await authService.login()
    NotificationCenter.default.removeObserver(observer)
}

// Expected count -- event must fire exactly N times
await confirmation("Received 3 items", expectedCount: 3) { confirm in
    processor.onItem = { _ in confirm() }
    await processor.process(items)
}
```

### Known Issues

```swift
// Known failing test -- does not count as failure
withKnownIssue("Propane tank is empty") {
    #expect(truck.grill.isHeating)
}

// Intermittent / flaky
withKnownIssue(isIntermittent: true) {
    #expect(service.isReachable)
}

// Conditional
withKnownIssue {
    #expect(foodTruck.grill.isHeating)
} when: {
    !hasPropane
}

// Match specific issues only
try withKnownIssue {
    let level = try #require(foodTruck.batteryLevel)
    #expect(level >= 0.8)
} matching: { issue in
    guard case .expectationFailed(let expectation) = issue.kind else { return false }
    return expectation.isRequired
}
```

If no known issues are recorded, Swift Testing records a distinct issue notifying you the problem may be resolved.

## Tags

Tags must be declared as static members in an extension on `Tag`:

```swift
extension Tag {
    @Tag static var critical: Self
    @Tag static var slow: Self
    @Tag static var networking: Self
    @Tag static var validation: Self
}

@Test(.tags(.critical, .networking))
func apiCallReturnsData() async throws { /* ... */ }
```

Filter tests by tag in Xcode test plans or CLI (`swift test --filter tag:critical`).

## TestScoping and Test Organization

`TestScoping` consolidates per-test setup/teardown into reusable fixtures:

```swift
struct DatabaseFixture: TestScoping {
    let db: TestDatabase

    func provideScope(
        for test: Test, testCase: Test.Case?,
        performing body: @Sendable () async throws -> Void
    ) async throws {
        let db = try await TestDatabase.create()
        try await body()
        try await db.destroy()
    }
}

// Use with @Test trait
@Test(.tags(.database))
func insertsRecord() async throws {
    // DatabaseFixture.provideScope wraps this test
}
```

## Mocking and Test Doubles

Define testable boundaries with protocols:

```swift
protocol UserRepository: Sendable {
    func fetch(id: String) async throws -> User
    func save(_ user: User) async throws
}

struct MockUserRepository: UserRepository {
    var users: [String: User] = [:]
    var fetchError: Error?
    var savedUsers: [User] = []

    func fetch(id: String) async throws -> User {
        if let error = fetchError { throw error }
        guard let user = users[id] else { throw NotFoundError() }
        return user
    }

    mutating func save(_ user: User) async throws {
        savedUsers.append(user)
        users[user.id] = user
    }
}
```

**Pattern:** Mocks conform to protocols, never subclass concrete types. Store call counts and arguments for verification.

## Testable Architecture

Inject dependencies through initializers for testability:

```swift
@Observable
class ProfileViewModel {
    var user: User?
    var error: Error?
    private let repository: UserRepository

    init(repository: UserRepository) {
        self.repository = repository
    }

    func load() async {
        do {
            user = try await repository.fetch(id: "current")
        } catch {
            self.error = error
        }
    }
}

// Test with mock
@Test @MainActor func viewModelLoadsUser() async {
    let mock = MockUserRepository(users: ["current": .preview])
    let vm = ProfileViewModel(repository: mock)
    await vm.load()
    #expect(vm.user?.name == "Alice")
}

@Test @MainActor func viewModelHandlesError() async {
    var mock = MockUserRepository()
    mock.fetchError = URLError(.notConnectedToInternet)
    let vm = ProfileViewModel(repository: mock)
    await vm.load()
    #expect(vm.user == nil)
    #expect(vm.error != nil)
}
```

## Async and Concurrent Tests

```swift
@Test @MainActor func viewModelUpdatesOnMainActor() async {
    let vm = ProfileViewModel(repository: MockUserRepository())
    await vm.load()
    #expect(vm.user != nil)
}

// Clock injection for time-dependent logic
@Test func debounceUsesCorrectDelay() async throws {
    let clock = TestClock()
    let debouncer = Debouncer(delay: .seconds(1), clock: clock)
    debouncer.submit { /* action */ }
    await clock.advance(by: .milliseconds(500))
    #expect(!debouncer.hasExecuted)
    await clock.advance(by: .milliseconds(500))
    #expect(debouncer.hasExecuted)
}

// Error path testing
@Test func fetchThrowsOnNetworkError() async {
    var mock = MockUserRepository()
    mock.fetchError = URLError(.notConnectedToInternet)
    #expect(throws: URLError.self) {
        try await mock.fetch(id: "1")
    }
}
```

## XCTest UI Tests — Page Object Pattern

Swift Testing does not support UI testing. Use XCTest with XCUITest for all UI tests.

```swift
class LoginUITests: XCTestCase {
    let app = XCUIApplication()

    override func setUpWithError() throws {
        continueAfterFailure = false
        app.launch()
    }

    func testLoginFlow() throws {
        let loginPage = LoginPage(app: app)
        let homePage = loginPage.login(email: "test@test.com", password: "password")
        XCTAssertTrue(homePage.welcomeLabel.exists)
    }
}
```

### Page Object Pattern

Encapsulate UI element queries in page objects for reusable, readable UI tests:

```swift
struct LoginPage {
    let app: XCUIApplication
    var emailField: XCUIElement { app.textFields["Email"] }
    var passwordField: XCUIElement { app.secureTextFields["Password"] }
    var signInButton: XCUIElement { app.buttons["Sign In"] }

    @discardableResult
    func login(email: String, password: String) -> HomePage {
        emailField.tap(); emailField.typeText(email)
        passwordField.tap(); passwordField.typeText(password)
        signInButton.tap()
        return HomePage(app: app)
    }
}

struct HomePage {
    let app: XCUIApplication
    var welcomeLabel: XCUIElement { app.staticTexts["Welcome"] }
}
```

## Performance Testing

```swift
class FeedPerformanceTests: XCTestCase {
    func testFeedParsingPerformance() throws {
        let data = try loadFixture("large-feed.json")
        let metrics: [XCTMetric] = [XCTClockMetric(), XCTMemoryMetric()]
        measure(metrics: metrics) {
            _ = try? FeedParser.parse(data)
        }
    }
}
```

Performance tests require XCTest — not available in Swift Testing.

## Snapshot Testing

Use swift-snapshot-testing (pointfreeco) for visual regression. Requires XCTest:

```swift
import SnapshotTesting
import XCTest

class ProfileViewSnapshotTests: XCTestCase {
    func testProfileView() {
        let view = ProfileView(user: .preview)
        assertSnapshot(of: view, as: .image(layout: .device(config: .iPhone13)))

        // Dark mode
        assertSnapshot(of: view.environment(\.colorScheme, .dark),
                       as: .image(layout: .device(config: .iPhone13)), named: "dark")

        // Large Dynamic Type
        assertSnapshot(of: view.environment(\.dynamicTypeSize, .accessibility3),
                       as: .image(layout: .device(config: .iPhone13)), named: "largeText")
    }
}
```

Always test Dark Mode and large Dynamic Type in snapshots.

## Test Attachments

Attach diagnostic data to test results for debugging failures:

```swift
@Test func generateReport() async throws {
    let report = try generateReport()
    // Attach the output for later inspection
    Attachment(report.data, named: "report.json").record()
    #expect(report.isValid)
}

// Attach from a file URL
@Test func processImage() async throws {
    let output = try processImage()
    try await Attachment(contentsOf: output.url, named: "result.png")
        .record()
}
```

Attachments support any `Attachable` type and images via `AttachableAsImage`.

## Exit Testing

Test code that calls `exit()`, `fatalError()`, or `preconditionFailure()`:

```swift
@Test func invalidInputCausesExit() async {
    await #expect(processExitsWith: .failure) {
        processInvalidInput()  // calls fatalError()
    }
}
```

Exit testing runs the closure in a subprocess. The test passes if the process exits with the expected status.

## Test File Organization

```text
Tests/AppTests/          # Swift Testing (Models/, ViewModels/, Services/)
Tests/AppUITests/        # XCTest UI tests (Pages/, Flows/)
Tests/Fixtures/          # Test data (JSON, images)
Tests/Mocks/             # Shared mock implementations
```

Name test files `<TypeUnderTest>Tests.swift`. Describe behavior in function names: `fetchUserReturnsNilOnNetworkError()` not `testFetchUser()`. Name mocks `Mock<ProtocolName>`.

### What to Test

**Always test:** business logic, validation rules, state transitions in view models, error handling paths, edge cases (empty collections, nil, boundaries), async success and failure, Task cancellation.

**Skip:** SwiftUI view body layout (use snapshots), simple property forwarding, Apple framework behavior, private methods (test through public API).

## Common Mistakes and Review Checklist

1. **Testing implementation, not behavior.** Test what the code does, not how.
2. **No error path tests.** If a function can throw, test the throw path.
3. **Flaky async tests.** Use `confirmation` with expected counts, not `sleep` calls.
4. **Shared mutable state between tests.** Each test sets up its own state via `init()` in `@Suite`.
5. **Missing accessibility identifiers in UI tests.** XCUITest queries rely on them.
6. **Using `sleep` in tests.** Use `confirmation`, clock injection, or `withKnownIssue`.
7. **Not testing cancellation.** If code supports `Task` cancellation, verify it cancels cleanly.
8. **Mixing XCTest and Swift Testing in one file.** Keep them in separate files.
9. **Non-Sendable test helpers shared across tests.** Ensure test helper types are Sendable when shared across concurrent test cases.

### Review Checklist

- [ ] All new tests use Swift Testing (`@Test`, `#expect`), not XCTest assertions
- [ ] Test names describe behavior (`fetchUserReturnsNilOnNetworkError` not `testFetchUser`)
- [ ] Error paths have dedicated tests
- [ ] Async tests use `confirmation()`, not `Task.sleep`
- [ ] Parameterized tests used for repetitive variations
- [ ] Tags applied for filtering (`.critical`, `.slow`)
- [ ] Mocks conform to protocols, not subclass concrete types
- [ ] No shared mutable state between tests
- [ ] Cancellation tested for cancellable async operations
