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

224 lines
5.3 KiB
Markdown

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