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>
This commit is contained in:
223
.planning/codebase/TESTING.md
Normal file
223
.planning/codebase/TESTING.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# 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*
|
||||
Reference in New Issue
Block a user