257 lines
8.1 KiB
Swift
257 lines
8.1 KiB
Swift
//
|
|
// 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: TestClock.now,
|
|
departureDate: TestClock.now.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: TestClock.now,
|
|
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
|
|
}
|
|
}
|