# Testing Patterns

**Analysis Date:** 2026-01-09

## Test Framework

**Runner:**
- Swift Testing (Apple's new testing framework, iOS 26+)
- Config: Built into Xcode, no separate config file

**Assertion Library:**
- `#expect()` macro (289 occurrences)
- Replaces XCTest's `XCTAssertEqual`, etc.

**Run Commands:**
```bash
# Run all tests
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' test

# Run specific test suite
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TravelEstimatorTests test

# Run single test
xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' -only-testing:SportsTimeTests/TestClassName/testMethodName test
```

## Test File Organization

**Location:**
- `SportsTimeTests/*.swift` - Unit tests
- `SportsTimeUITests/*.swift` - UI tests (XCTest-based)

**Naming:**
- Unit tests: `{Component}Tests.swift`
- No integration/e2e distinction in filename

**Structure:**
```
SportsTimeTests/
├── TravelEstimatorTests.swift        # 50+ tests
├── SportsTimeTests.swift             # DayCard tests (11+), regression tests
├── ScenarioAPlannerSwiftTests.swift  # 28 tests
├── ScenarioBPlannerTests.swift       # 44 tests
├── ScenarioCPlannerTests.swift       # 49 tests
└── (total: 180+ unit tests)
```

## Test Structure

**Suite Organization:**
```swift
import Testing
import Foundation
@testable import SportsTime

@Suite("ScenarioBPlanner Tests")
struct ScenarioBPlannerTests {

    // MARK: - Test Fixtures

    private func makeStadium(...) -> Stadium { ... }
    private func makeGame(...) -> Game { ... }

    // MARK: - Tests

    @Test("handles empty game list")
    func emptyGameList() {
        // arrange
        // act
        // assert with #expect()
    }
}
```

**Patterns:**
- `@Suite("Description")` for grouping related tests
- `@Test("Description")` for individual tests (not `func test...`)
- `#expect()` for assertions
- Private `make*` factory functions for test fixtures

## Mocking

**Framework:**
- No external mocking framework
- Manual test doubles via protocol conformance

**Patterns:**
```swift
// Factory functions create test data
private func makeGame(
    id: UUID = UUID(),
    stadiumId: UUID,
    date: Date
) -> Game {
    Game(
        id: id,
        homeTeamId: UUID(),
        awayTeamId: UUID(),
        stadiumId: stadiumId,
        dateTime: date,
        sport: .mlb,
        season: "2026"
    )
}
```

**What to Mock:**
- External services (CloudKit, network)
- Date/time (use fixed dates in tests)

**What NOT to Mock:**
- Pure functions (TravelEstimator calculations)
- Domain models

## Fixtures and Factories

**Test Data:**
```swift
// Factory pattern in test structs
private func makeStadium(
    id: UUID = UUID(),
    name: String,
    city: String,
    state: String,
    latitude: Double,
    longitude: Double,
    sport: Sport = .mlb
) -> Stadium { ... }

private func date(_ string: String) -> Date {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd HH:mm"
    formatter.timeZone = TimeZone(identifier: "America/Los_Angeles")
    return formatter.date(from: string)!
}

private func defaultConstraints() -> DrivingConstraints { ... }
```

**Location:**
- Factory functions: Defined in each test struct under `// MARK: - Test Fixtures`
- No shared fixtures directory

## Coverage

**Requirements:**
- No enforced coverage target
- Focus on critical paths (planning engine, travel estimation)

**Configuration:**
- Xcode built-in coverage via scheme settings
- No separate coverage tool

## Test Types

**Unit Tests (SportsTimeTests/):**
- Test single function/component in isolation
- Pure logic tests (no network, no persistence)
- Fast: milliseconds per test
- Examples: `TravelEstimatorTests`, `ScenarioAPlannerTests`

**UI Tests (SportsTimeUITests/):**
- XCTest-based (older framework)
- Test user flows end-to-end
- Slower, requires simulator

## Common Patterns

**Async Testing:**
```swift
@Test("async operation succeeds")
func asyncOperation() async {
    let result = await asyncFunction()
    #expect(result == expected)
}
```

**Error Testing:**
```swift
@Test("throws on invalid input")
func invalidInput() throws {
    #expect(throws: SomeError.self) {
        try functionThatThrows()
    }
}
```

**Known Distance Testing:**
```swift
@Test("LA to SF distance is approximately 350 miles")
func laToSfDistance() {
    let distance = TravelEstimator.haversineDistanceMiles(
        from: Coordinate(latitude: 34.05, longitude: -118.24),
        to: Coordinate(latitude: 37.77, longitude: -122.42)
    )
    // Known distance is ~350 miles
    #expect(distance > 340 && distance < 360)
}
```

**Regression Test Pattern:**
```swift
// Regression test for handling duplicate game IDs without crashing
@Test("deduplicates games with same ID")
func duplicateGameHandling() {
    // Setup with duplicate IDs
    // Verify first occurrence preserved
    // Verify no crash
}
```

## Bug Fix Protocol

From `CLAUDE.md`:
1. Write failing test that reproduces bug
2. Fix the bug
3. Verify test passes along with all existing tests
4. Name tests descriptively: `test_Component_Condition_Expected`

---

*Testing analysis: 2026-01-09*
*Update when test patterns change*
