# Unit Test Rewrite Design ## Goal Delete all existing broken tests and create comprehensive new test coverage using Swift Testing framework, covering all edge cases for Planning Engine, Domain Models, and Services. ## Requirements - **Framework:** Swift Testing (`@Test`, `#expect`, `@Suite`) - **Structure:** Mirror source folder layout - **Parallelization:** Tests run in parallel across multiple simulator devices - **Coverage:** ~52 source files, ~150-200 tests total ## Test Structure ``` SportsTimeTests/ ├── Planning/ │ ├── TripPlanningEngineTests.swift │ ├── GameDAGRouterTests.swift │ ├── ItineraryBuilderTests.swift │ ├── RouteFiltersTests.swift │ ├── TravelEstimatorTests.swift │ ├── ScenarioPlannerTests.swift │ ├── ScenarioAPlannerTests.swift │ ├── ScenarioBPlannerTests.swift │ ├── ScenarioCPlannerTests.swift │ ├── ScenarioDPlannerTests.swift │ └── PlanningModelsTests.swift ├── Domain/ │ ├── GameTests.swift │ ├── TripTests.swift │ ├── TripStopTests.swift │ ├── StadiumTests.swift │ ├── TeamTests.swift │ ├── SportTests.swift │ ├── RegionTests.swift │ ├── DivisionTests.swift │ ├── TravelSegmentTests.swift │ ├── ProgressTests.swift │ ├── TripPreferencesTests.swift │ ├── DynamicSportTests.swift │ ├── TripPollTests.swift │ ├── AnySportTests.swift │ └── AchievementDefinitionsTests.swift ├── Services/ │ ├── CloudKitServiceTests.swift │ ├── DataProviderTests.swift │ ├── LocationServiceTests.swift │ ├── AchievementEngineTests.swift │ ├── PollServiceTests.swift │ ├── GameMatcherTests.swift │ ├── EVChargingServiceTests.swift │ ├── StadiumProximityMatcherTests.swift │ ├── RouteDescriptionGeneratorTests.swift │ ├── SuggestedTripsGeneratorTests.swift │ └── (remaining services...) └── Helpers/ ├── TestFixtures.swift └── MockServices.swift ``` ## Parallel Execution Configuration ### Xcode Test Plan (`SportsTimeTests.xctestplan`) ```json { "configurations": [ { "name": "Parallel Configuration", "options": { "targetForVariableExpansion": { "containerPath": "container:SportsTime.xcodeproj", "identifier": "SportsTimeTests" }, "maximumParallelSimulators": 4, "parallelExecutionEnabled": true } } ], "defaultOptions": { "maximumParallelSimulators": 4, "parallelExecutionEnabled": true } } ``` ### Command Line Parallel Execution ```bash # Run tests in parallel across 4 simulators xcodebuild test \ -project SportsTime.xcodeproj \ -scheme SportsTime \ -parallel-testing-enabled YES \ -parallel-testing-worker-count 4 \ -destination 'platform=iOS Simulator,name=iPhone 17' \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro Max' \ -destination 'platform=iOS Simulator,name=iPad Pro 13-inch (M4)' ``` ### Swift Testing Parallelization Swift Testing runs tests in parallel by default. For tests that must run serially (shared state), use: ```swift @Suite(.serialized) struct DatabaseTests { // These tests run one at a time } ``` ## Test Patterns ### Standard Test Structure ```swift import Testing @testable import SportsTime @Suite("GameDAGRouter") struct GameDAGRouterTests { // MARK: - findRoutes @Test("Returns valid routes for standard input") func findRoutes_validInput_returnsRoutes() async { let games = TestFixtures.games(count: 5) let stadiums = TestFixtures.stadiumMap(for: games) let routes = GameDAGRouter.findRoutes( games: games, stadiums: stadiums, maxDailyDrivingMiles: 500, beamWidth: 10 ) #expect(!routes.isEmpty) #expect(routes.allSatisfy { $0.count <= games.count }) } @Test("Returns empty for empty game list") func findRoutes_emptyGames_returnsEmpty() async { let routes = GameDAGRouter.findRoutes( games: [], stadiums: [:], maxDailyDrivingMiles: 500, beamWidth: 10 ) #expect(routes.isEmpty) } @Test("Handles single game") func findRoutes_singleGame_returnsSingleRoute() async { let game = TestFixtures.game() let stadiums = TestFixtures.stadiumMap(for: [game]) let routes = GameDAGRouter.findRoutes( games: [game], stadiums: stadiums, maxDailyDrivingMiles: 500, beamWidth: 10 ) #expect(routes.count == 1) #expect(routes.first?.count == 1) } // MARK: - Parameterized Scale Tests @Test("Scales with game count", arguments: [5, 10, 25, 50, 100]) func findRoutes_scales(gameCount: Int) async { let games = TestFixtures.games(count: gameCount) let stadiums = TestFixtures.stadiumMap(for: games) let routes = GameDAGRouter.findRoutes( games: games, stadiums: stadiums, maxDailyDrivingMiles: 500, beamWidth: 10 ) #expect(routes.count > 0) } // MARK: - canTransition Edge Cases @Test("Rejects impossible same-day transitions") func canTransition_impossibleSameDay_returnsFalse() async { let nyGame = TestFixtures.game(city: "New York", date: Date()) let laGame = TestFixtures.game(city: "Los Angeles", date: Date()) let canTransit = GameDAGRouter.canTransition( from: nyGame, to: laGame, maxDailyDrivingMiles: 500 ) #expect(!canTransit) } } ``` ### Test Fixtures ```swift // TestFixtures.swift import Foundation @testable import SportsTime enum TestFixtures { // MARK: - Games static func game( id: String = UUID().uuidString, sport: Sport = .mlb, city: String = "New York", date: Date = Date(), homeTeamId: String = "nyy", awayTeamId: String = "bos" ) -> Game { Game( id: id, sport: sport, homeTeamId: homeTeamId, awayTeamId: awayTeamId, stadiumId: "\(city.lowercased())-stadium", dateTime: date ) } static func games(count: Int, spread: TimeInterval = 86400) -> [Game] { (0.. Stadium { Stadium( id: id, name: name, city: city, state: "NY", latitude: latitude, longitude: longitude, sport: .mlb, teamIds: ["test-team"] ) } static func stadiumMap(for games: [Game]) -> [String: Stadium] { var map: [String: Stadium] = [:] for game in games { if map[game.stadiumId] == nil { map[game.stadiumId] = stadium(id: game.stadiumId) } } return map } // MARK: - Trips static func trip( id: String = UUID().uuidString, stops: [TripStop] = [] ) -> Trip { Trip( id: id, name: "Test Trip", stops: stops, createdAt: Date(), status: .planned ) } // MARK: - Planning Requests static func planningRequest( mode: PlanningMode = .dateRange, sports: Set = [.mlb], startDate: Date = Date(), endDate: Date = Date().addingTimeInterval(86400 * 7), regions: Set = [.east] ) -> PlanningRequest { PlanningRequest( mode: mode, sports: sports, startDate: startDate, endDate: endDate, regions: regions, routePreference: .balanced, allowRepeatCities: false, mustStopLocations: [] ) } } ``` ### Mock Services ```swift // MockServices.swift import Foundation @testable import SportsTime // MARK: - Mock CloudKit Service final class MockCloudKitService: CloudKitServiceProtocol { var stubbedGames: [Game] = [] var stubbedStadiums: [Stadium] = [] var stubbedTeams: [Team] = [] var shouldFail = false var failureError: Error = NSError(domain: "test", code: -1) func fetchGames() async throws -> [Game] { if shouldFail { throw failureError } return stubbedGames } func fetchStadiums() async throws -> [Stadium] { if shouldFail { throw failureError } return stubbedStadiums } func fetchTeams() async throws -> [Team] { if shouldFail { throw failureError } return stubbedTeams } } // MARK: - Mock Location Service final class MockLocationService: LocationServiceProtocol { var stubbedDistance: Double = 100.0 var stubbedTravelTime: TimeInterval = 3600 var shouldFail = false func calculateDistance(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) async throws -> Double { if shouldFail { throw LocationError.unavailable } return stubbedDistance } func calculateTravelTime(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D) async throws -> TimeInterval { if shouldFail { throw LocationError.unavailable } return stubbedTravelTime } } ``` ## Edge Cases by Module ### Planning Engine | Function | Edge Cases | |----------|------------| | `planItineraries` | Empty request, past dates, single day, 30+ days, no matching games | | `findRoutes` | 0 games, 1 game, 100+ games, all same city, all different cities | | `canTransition` | Same day/different coast, next day/same city, impossible distances | | `build` | Empty route, circular route, gaps in dates | | `filterRepeatCities` | No repeats, all repeats, alternating cities | | `estimate` | Zero distance, cross-country, international coords | ### Domain Models | Model | Edge Cases | |-------|------------| | `Game` | Nil teams, past dates, invalid stadium ID | | `Trip` | Empty stops, single stop, duplicate stops | | `Stadium` | Edge coordinates (0,0), (90,180), negative values | | `Region.classify` | Boundary longitudes (-85, -110), exact boundaries | | `Sport` | All enum cases, raw value round-trip | ### Services | Service | Edge Cases | |---------|------------| | `CloudKitService` | Network failure, empty response, partial data, quota exceeded | | `DataProvider` | Not configured, empty cache, stale data | | `LocationService` | No permission, unavailable, invalid coordinates | | `AchievementEngine` | No visits, first visit, duplicate check | ## Implementation Order 1. **Delete existing tests** - Remove all 38 broken test files 2. **Create infrastructure** - TestFixtures.swift, MockServices.swift 3. **Planning tests** (highest priority - core business logic) 4. **Domain tests** (data integrity) 5. **Services tests** (integrations) ## Success Criteria - All tests pass - Tests run in parallel across 4 simulators - No flaky tests (deterministic, no timing dependencies) - Coverage of all public functions - Edge cases documented and tested