refactor(tests): TDD rewrite of all unit tests with spec documentation
Complete rewrite of unit test suite using TDD methodology: Planning Engine Tests: - GameDAGRouterTests: Beam search, anchor games, transitions - ItineraryBuilderTests: Stop connection, validators, EV enrichment - RouteFiltersTests: Region, time window, scoring filters - ScenarioA/B/C/D PlannerTests: All planning scenarios - TravelEstimatorTests: Distance, duration, travel days - TripPlanningEngineTests: Orchestration, caching, preferences Domain Model Tests: - AchievementDefinitionsTests, AnySportTests, DivisionTests - GameTests, ProgressTests, RegionTests, StadiumTests - TeamTests, TravelSegmentTests, TripTests, TripPollTests - TripPreferencesTests, TripStopTests, SportTests Service Tests: - FreeScoreAPITests, RouteDescriptionGeneratorTests - SuggestedTripsGeneratorTests Export Tests: - ShareableContentTests (card types, themes, dimensions) Bug fixes discovered through TDD: - ShareCardDimensions: mapSnapshotSize exceeded available width (960x480) - ScenarioBPlanner: Added anchor game validation filter All tests include: - Specification tests (expected behavior) - Invariant tests (properties that must always hold) - Edge case tests (boundary conditions) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
256
SportsTimeTests/Services/RouteDescriptionGeneratorTests.swift
Normal file
256
SportsTimeTests/Services/RouteDescriptionGeneratorTests.swift
Normal file
@@ -0,0 +1,256 @@
|
||||
//
|
||||
// RouteDescriptionGeneratorTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// TDD specification tests for RouteDescriptionGenerator types.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
@testable import SportsTime
|
||||
|
||||
// MARK: - RouteDescriptionInput Tests
|
||||
|
||||
@Suite("RouteDescriptionInput")
|
||||
struct RouteDescriptionInputTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private let nycCoord = CLLocationCoordinate2D(latitude: 40.7580, longitude: -73.9855)
|
||||
private let bostonCoord = CLLocationCoordinate2D(latitude: 42.3467, longitude: -71.0972)
|
||||
|
||||
private func makeOption(
|
||||
stops: [ItineraryStop] = [],
|
||||
totalDrivingHours: Double = 8.5,
|
||||
totalDistanceMiles: Double = 500
|
||||
) -> ItineraryOption {
|
||||
ItineraryOption(
|
||||
rank: 1,
|
||||
stops: stops,
|
||||
travelSegments: [],
|
||||
totalDrivingHours: totalDrivingHours,
|
||||
totalDistanceMiles: totalDistanceMiles,
|
||||
geographicRationale: "Test rationale"
|
||||
)
|
||||
}
|
||||
|
||||
private func makeStop(city: String, games: [String] = []) -> ItineraryStop {
|
||||
ItineraryStop(
|
||||
city: city,
|
||||
state: "XX",
|
||||
coordinate: nycCoord,
|
||||
games: games,
|
||||
arrivalDate: Date(),
|
||||
departureDate: Date().addingTimeInterval(86400),
|
||||
location: LocationInput(name: city, coordinate: nycCoord),
|
||||
firstGameStart: nil
|
||||
)
|
||||
}
|
||||
|
||||
private func makeRichGame(id: String, sport: Sport = .mlb) -> RichGame {
|
||||
let game = Game(
|
||||
id: id,
|
||||
homeTeamId: "team1",
|
||||
awayTeamId: "team2",
|
||||
stadiumId: "stadium1",
|
||||
dateTime: Date(),
|
||||
sport: sport,
|
||||
season: "2026",
|
||||
isPlayoff: false
|
||||
)
|
||||
let team = Team(
|
||||
id: "team1",
|
||||
name: "Test Team",
|
||||
abbreviation: "TST",
|
||||
sport: sport,
|
||||
city: "Test City",
|
||||
stadiumId: "stadium1"
|
||||
)
|
||||
let stadium = Stadium(
|
||||
id: "stadium1",
|
||||
name: "Test Stadium",
|
||||
city: "Test City",
|
||||
state: "XX",
|
||||
latitude: nycCoord.latitude,
|
||||
longitude: nycCoord.longitude,
|
||||
capacity: 40000,
|
||||
sport: sport
|
||||
)
|
||||
return RichGame(game: game, homeTeam: team, awayTeam: team, stadium: stadium)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: init(from:games:)
|
||||
|
||||
@Test("init: extracts cities from option stops")
|
||||
func init_extractsCities() {
|
||||
let stops = [
|
||||
makeStop(city: "New York"),
|
||||
makeStop(city: "Boston"),
|
||||
makeStop(city: "Philadelphia")
|
||||
]
|
||||
let option = makeOption(stops: stops)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.cities == ["New York", "Boston", "Philadelphia"])
|
||||
}
|
||||
|
||||
@Test("init: deduplicates cities preserving order")
|
||||
func init_deduplicatesCities() {
|
||||
let stops = [
|
||||
makeStop(city: "New York"),
|
||||
makeStop(city: "Boston"),
|
||||
makeStop(city: "New York") // Duplicate
|
||||
]
|
||||
let option = makeOption(stops: stops)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
// NSOrderedSet preserves first occurrence order and removes duplicates
|
||||
#expect(input.cities == ["New York", "Boston"])
|
||||
}
|
||||
|
||||
@Test("init: extracts sports from games")
|
||||
func init_extractsSports() {
|
||||
let stops = [makeStop(city: "New York", games: ["game1", "game2"])]
|
||||
let option = makeOption(stops: stops)
|
||||
let games = [
|
||||
"game1": makeRichGame(id: "game1", sport: .mlb),
|
||||
"game2": makeRichGame(id: "game2", sport: .nba)
|
||||
]
|
||||
let input = RouteDescriptionInput(from: option, games: games)
|
||||
|
||||
#expect(input.sports.count == 2)
|
||||
#expect(input.sports.contains("MLB")) // rawValue is uppercase
|
||||
#expect(input.sports.contains("NBA")) // rawValue is uppercase
|
||||
}
|
||||
|
||||
@Test("init: computes totalGames from option")
|
||||
func init_computesTotalGames() {
|
||||
let stops = [
|
||||
makeStop(city: "New York", games: ["g1", "g2"]),
|
||||
makeStop(city: "Boston", games: ["g3"])
|
||||
]
|
||||
let option = makeOption(stops: stops)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.totalGames == 3)
|
||||
}
|
||||
|
||||
@Test("init: copies totalMiles from option")
|
||||
func init_copiesTotalMiles() {
|
||||
let option = makeOption(totalDistanceMiles: 1234.5)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.totalMiles == 1234.5)
|
||||
}
|
||||
|
||||
@Test("init: copies totalDrivingHours from option")
|
||||
func init_copiesTotalDrivingHours() {
|
||||
let option = makeOption(totalDrivingHours: 15.75)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.totalDrivingHours == 15.75)
|
||||
}
|
||||
|
||||
@Test("init: copies id from option")
|
||||
func init_copiesId() {
|
||||
let option = makeOption()
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.id == option.id)
|
||||
}
|
||||
|
||||
// MARK: - Edge Cases
|
||||
|
||||
@Test("init: handles empty stops")
|
||||
func init_emptyStops() {
|
||||
let option = makeOption(stops: [])
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.cities.isEmpty)
|
||||
#expect(input.totalGames == 0)
|
||||
}
|
||||
|
||||
@Test("init: handles empty games dictionary")
|
||||
func init_emptyGames() {
|
||||
let stops = [makeStop(city: "NYC", games: ["g1"])]
|
||||
let option = makeOption(stops: stops)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.sports.isEmpty)
|
||||
}
|
||||
|
||||
@Test("init: handles zero distance and hours")
|
||||
func init_zeroValues() {
|
||||
let option = makeOption(totalDrivingHours: 0, totalDistanceMiles: 0)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.totalMiles == 0)
|
||||
#expect(input.totalDrivingHours == 0)
|
||||
}
|
||||
|
||||
@Test("init: handles single city")
|
||||
func init_singleCity() {
|
||||
let stops = [makeStop(city: "Only City")]
|
||||
let option = makeOption(stops: stops)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.cities.count == 1)
|
||||
#expect(input.cities.first == "Only City")
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
/// - Invariant: cities preserves stop order
|
||||
@Test("Invariant: cities preserves stop order")
|
||||
func invariant_citiesPreservesOrder() {
|
||||
let stops = [
|
||||
makeStop(city: "First"),
|
||||
makeStop(city: "Second"),
|
||||
makeStop(city: "Third")
|
||||
]
|
||||
let option = makeOption(stops: stops)
|
||||
let input = RouteDescriptionInput(from: option, games: [:])
|
||||
|
||||
#expect(input.cities == ["First", "Second", "Third"])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RouteDescription Tests
|
||||
|
||||
@Suite("RouteDescription")
|
||||
struct RouteDescriptionTests {
|
||||
|
||||
// MARK: - Specification Tests
|
||||
|
||||
/// - Expected Behavior: RouteDescription stores description string
|
||||
@Test("RouteDescription: stores description")
|
||||
func routeDescription_storesDescription() {
|
||||
let desc = RouteDescription(description: "An exciting road trip!")
|
||||
#expect(desc.description == "An exciting road trip!")
|
||||
}
|
||||
|
||||
@Test("RouteDescription: handles empty description")
|
||||
func routeDescription_emptyDescription() {
|
||||
let desc = RouteDescription(description: "")
|
||||
#expect(desc.description == "")
|
||||
}
|
||||
|
||||
@Test("RouteDescription: handles long description")
|
||||
func routeDescription_longDescription() {
|
||||
let longText = String(repeating: "A", count: 1000)
|
||||
let desc = RouteDescription(description: longText)
|
||||
#expect(desc.description == longText)
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
/// - Invariant: description is never nil (non-optional)
|
||||
@Test("Invariant: description is non-optional")
|
||||
func invariant_descriptionNonOptional() {
|
||||
let desc = RouteDescription(description: "Test")
|
||||
// Just accessing .description should always work
|
||||
let _ = desc.description
|
||||
#expect(Bool(true)) // If we got here, description is non-optional
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user