Comprehensive plan to delete broken tests and create new Swift Testing coverage for Planning Engine, Domain Models, and Services with parallel execution across multiple simulators. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
11 KiB
11 KiB
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)
{
"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
# 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:
@Suite(.serialized)
struct DatabaseTests {
// These tests run one at a time
}
Test Patterns
Standard Test Structure
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
// 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..<count).map { i in
game(
id: "game-\(i)",
date: Date().addingTimeInterval(Double(i) * spread)
)
}
}
// MARK: - Stadiums
static func stadium(
id: String = UUID().uuidString,
name: String = "Test Stadium",
city: String = "New York",
latitude: Double = 40.7128,
longitude: Double = -74.0060
) -> 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<Sport> = [.mlb],
startDate: Date = Date(),
endDate: Date = Date().addingTimeInterval(86400 * 7),
regions: Set<Region> = [.east]
) -> PlanningRequest {
PlanningRequest(
mode: mode,
sports: sports,
startDate: startDate,
endDate: endDate,
regions: regions,
routePreference: .balanced,
allowRepeatCities: false,
mustStopLocations: []
)
}
}
Mock Services
// 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
- Delete existing tests - Remove all 38 broken test files
- Create infrastructure - TestFixtures.swift, MockServices.swift
- Planning tests (highest priority - core business logic)
- Domain tests (data integrity)
- 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