# 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*