Files
Sportstime/.planning/codebase/TESTING.md
Trey t 60b450d869 docs: add Phase 1 plans and codebase documentation
- 01-01-PLAN.md: core.py + mlb.py (executed)
- 01-02-PLAN.md: nba.py + nhl.py
- 01-03-PLAN.md: nfl.py + orchestrator refactor
- Codebase documentation for planning context

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 00:00:45 -06:00

5.3 KiB

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:

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

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:

// 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:

// 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:

@Test("async operation succeeds")
func asyncOperation() async {
    let result = await asyncFunction()
    #expect(result == expected)
}

Error Testing:

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

Known Distance Testing:

@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:

// 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