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:
375
SportsTimeTests/Services/FreeScoreAPITests.swift
Normal file
375
SportsTimeTests/Services/FreeScoreAPITests.swift
Normal file
@@ -0,0 +1,375 @@
|
||||
//
|
||||
// FreeScoreAPITests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// TDD specification tests for FreeScoreAPI types.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import SportsTime
|
||||
|
||||
// MARK: - ProviderReliability Tests
|
||||
|
||||
@Suite("ProviderReliability")
|
||||
struct ProviderReliabilityTests {
|
||||
|
||||
// MARK: - Specification Tests: Values
|
||||
|
||||
@Test("official: raw value is 'official'")
|
||||
func official_rawValue() {
|
||||
#expect(ProviderReliability.official.rawValue == "official")
|
||||
}
|
||||
|
||||
@Test("unofficial: raw value is 'unofficial'")
|
||||
func unofficial_rawValue() {
|
||||
#expect(ProviderReliability.unofficial.rawValue == "unofficial")
|
||||
}
|
||||
|
||||
@Test("scraped: raw value is 'scraped'")
|
||||
func scraped_rawValue() {
|
||||
#expect(ProviderReliability.scraped.rawValue == "scraped")
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
/// - Invariant: All reliability levels have distinct raw values
|
||||
@Test("Invariant: all raw values are distinct")
|
||||
func invariant_distinctRawValues() {
|
||||
let all: [ProviderReliability] = [.official, .unofficial, .scraped]
|
||||
let rawValues = all.map { $0.rawValue }
|
||||
let uniqueValues = Set(rawValues)
|
||||
#expect(rawValues.count == uniqueValues.count)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HistoricalGameQuery Tests
|
||||
|
||||
@Suite("HistoricalGameQuery")
|
||||
struct HistoricalGameQueryTests {
|
||||
|
||||
// MARK: - Specification Tests: normalizedDateString
|
||||
|
||||
/// - Expected Behavior: Returns date in yyyy-MM-dd format (Eastern time)
|
||||
@Test("normalizedDateString: formats as yyyy-MM-dd")
|
||||
func normalizedDateString_format() {
|
||||
var calendar = Calendar.current
|
||||
calendar.timeZone = TimeZone(identifier: "America/New_York")!
|
||||
let date = calendar.date(from: DateComponents(year: 2026, month: 6, day: 15))!
|
||||
|
||||
let query = HistoricalGameQuery(sport: .mlb, date: date)
|
||||
|
||||
#expect(query.normalizedDateString == "2026-06-15")
|
||||
}
|
||||
|
||||
@Test("normalizedDateString: pads single-digit months")
|
||||
func normalizedDateString_padMonth() {
|
||||
var calendar = Calendar.current
|
||||
calendar.timeZone = TimeZone(identifier: "America/New_York")!
|
||||
let date = calendar.date(from: DateComponents(year: 2026, month: 3, day: 5))!
|
||||
|
||||
let query = HistoricalGameQuery(sport: .mlb, date: date)
|
||||
|
||||
#expect(query.normalizedDateString == "2026-03-05")
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: Initialization
|
||||
|
||||
@Test("init: stores sport correctly")
|
||||
func init_storesSport() {
|
||||
let query = HistoricalGameQuery(sport: .nba, date: Date())
|
||||
#expect(query.sport == .nba)
|
||||
}
|
||||
|
||||
@Test("init: stores team abbreviations")
|
||||
func init_storesTeams() {
|
||||
let query = HistoricalGameQuery(
|
||||
sport: .mlb,
|
||||
date: Date(),
|
||||
homeTeamAbbrev: "NYY",
|
||||
awayTeamAbbrev: "BOS"
|
||||
)
|
||||
|
||||
#expect(query.homeTeamAbbrev == "NYY")
|
||||
#expect(query.awayTeamAbbrev == "BOS")
|
||||
}
|
||||
|
||||
@Test("init: team abbreviations default to nil")
|
||||
func init_defaultNilTeams() {
|
||||
let query = HistoricalGameQuery(sport: .mlb, date: Date())
|
||||
|
||||
#expect(query.homeTeamAbbrev == nil)
|
||||
#expect(query.awayTeamAbbrev == nil)
|
||||
#expect(query.stadiumCanonicalId == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HistoricalGameResult Tests
|
||||
|
||||
@Suite("HistoricalGameResult")
|
||||
struct HistoricalGameResultTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private func makeResult(
|
||||
homeScore: Int? = 5,
|
||||
awayScore: Int? = 3
|
||||
) -> HistoricalGameResult {
|
||||
HistoricalGameResult(
|
||||
sport: .mlb,
|
||||
gameDate: Date(),
|
||||
homeTeamAbbrev: "NYY",
|
||||
awayTeamAbbrev: "BOS",
|
||||
homeTeamName: "Yankees",
|
||||
awayTeamName: "Red Sox",
|
||||
homeScore: homeScore,
|
||||
awayScore: awayScore,
|
||||
source: .api,
|
||||
providerName: "Test Provider"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: scoreString
|
||||
|
||||
/// - Expected Behavior: Format is "away-home" (e.g., "3-5")
|
||||
@Test("scoreString: formats as away-home")
|
||||
func scoreString_format() {
|
||||
let result = makeResult(homeScore: 5, awayScore: 3)
|
||||
#expect(result.scoreString == "3-5")
|
||||
}
|
||||
|
||||
@Test("scoreString: nil when homeScore is nil")
|
||||
func scoreString_nilHomeScore() {
|
||||
let result = makeResult(homeScore: nil, awayScore: 3)
|
||||
#expect(result.scoreString == nil)
|
||||
}
|
||||
|
||||
@Test("scoreString: nil when awayScore is nil")
|
||||
func scoreString_nilAwayScore() {
|
||||
let result = makeResult(homeScore: 5, awayScore: nil)
|
||||
#expect(result.scoreString == nil)
|
||||
}
|
||||
|
||||
@Test("scoreString: nil when both scores are nil")
|
||||
func scoreString_bothNil() {
|
||||
let result = makeResult(homeScore: nil, awayScore: nil)
|
||||
#expect(result.scoreString == nil)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: hasScore
|
||||
|
||||
/// - Expected Behavior: true only when both scores are present
|
||||
@Test("hasScore: true when both scores present")
|
||||
func hasScore_bothPresent() {
|
||||
let result = makeResult(homeScore: 5, awayScore: 3)
|
||||
#expect(result.hasScore == true)
|
||||
}
|
||||
|
||||
@Test("hasScore: false when homeScore is nil")
|
||||
func hasScore_nilHomeScore() {
|
||||
let result = makeResult(homeScore: nil, awayScore: 3)
|
||||
#expect(result.hasScore == false)
|
||||
}
|
||||
|
||||
@Test("hasScore: false when awayScore is nil")
|
||||
func hasScore_nilAwayScore() {
|
||||
let result = makeResult(homeScore: 5, awayScore: nil)
|
||||
#expect(result.hasScore == false)
|
||||
}
|
||||
|
||||
@Test("hasScore: false when both are nil")
|
||||
func hasScore_bothNil() {
|
||||
let result = makeResult(homeScore: nil, awayScore: nil)
|
||||
#expect(result.hasScore == false)
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
/// - Invariant: hasScore == true implies scoreString != nil
|
||||
@Test("Invariant: hasScore implies scoreString exists")
|
||||
func invariant_hasScoreImpliesScoreString() {
|
||||
let withScore = makeResult(homeScore: 5, awayScore: 3)
|
||||
if withScore.hasScore {
|
||||
#expect(withScore.scoreString != nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Invariant: scoreString != nil implies hasScore
|
||||
@Test("Invariant: scoreString exists implies hasScore")
|
||||
func invariant_scoreStringImpliesHasScore() {
|
||||
let withScore = makeResult(homeScore: 5, awayScore: 3)
|
||||
if withScore.scoreString != nil {
|
||||
#expect(withScore.hasScore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ScoreResolutionResult Tests
|
||||
|
||||
@Suite("ScoreResolutionResult")
|
||||
struct ScoreResolutionResultTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private func makeHistoricalResult() -> HistoricalGameResult {
|
||||
HistoricalGameResult(
|
||||
sport: .mlb,
|
||||
gameDate: Date(),
|
||||
homeTeamAbbrev: "NYY",
|
||||
awayTeamAbbrev: "BOS",
|
||||
homeTeamName: "Yankees",
|
||||
awayTeamName: "Red Sox",
|
||||
homeScore: 5,
|
||||
awayScore: 3,
|
||||
source: .api,
|
||||
providerName: "Test"
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: isResolved
|
||||
|
||||
@Test("isResolved: true for resolved case")
|
||||
func isResolved_resolved() {
|
||||
let result = ScoreResolutionResult.resolved(makeHistoricalResult())
|
||||
#expect(result.isResolved == true)
|
||||
}
|
||||
|
||||
@Test("isResolved: false for pending case")
|
||||
func isResolved_pending() {
|
||||
let result = ScoreResolutionResult.pending
|
||||
#expect(result.isResolved == false)
|
||||
}
|
||||
|
||||
@Test("isResolved: false for requiresUserInput case")
|
||||
func isResolved_requiresUserInput() {
|
||||
let result = ScoreResolutionResult.requiresUserInput(reason: "Test reason")
|
||||
#expect(result.isResolved == false)
|
||||
}
|
||||
|
||||
@Test("isResolved: false for notFound case")
|
||||
func isResolved_notFound() {
|
||||
let result = ScoreResolutionResult.notFound(reason: "No game found")
|
||||
#expect(result.isResolved == false)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: result
|
||||
|
||||
@Test("result: returns HistoricalGameResult for resolved case")
|
||||
func result_resolved() {
|
||||
let historical = makeHistoricalResult()
|
||||
let result = ScoreResolutionResult.resolved(historical)
|
||||
|
||||
#expect(result.result != nil)
|
||||
#expect(result.result?.homeTeamAbbrev == "NYY")
|
||||
}
|
||||
|
||||
@Test("result: returns nil for pending case")
|
||||
func result_pending() {
|
||||
let result = ScoreResolutionResult.pending
|
||||
#expect(result.result == nil)
|
||||
}
|
||||
|
||||
@Test("result: returns nil for requiresUserInput case")
|
||||
func result_requiresUserInput() {
|
||||
let result = ScoreResolutionResult.requiresUserInput(reason: "Test")
|
||||
#expect(result.result == nil)
|
||||
}
|
||||
|
||||
@Test("result: returns nil for notFound case")
|
||||
func result_notFound() {
|
||||
let result = ScoreResolutionResult.notFound(reason: "Not found")
|
||||
#expect(result.result == nil)
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
/// - Invariant: isResolved == true implies result != nil
|
||||
@Test("Invariant: isResolved implies result exists")
|
||||
func invariant_isResolvedImpliesResult() {
|
||||
let resolved = ScoreResolutionResult.resolved(makeHistoricalResult())
|
||||
if resolved.isResolved {
|
||||
#expect(resolved.result != nil)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Invariant: isResolved == false implies result == nil
|
||||
@Test("Invariant: not resolved implies result nil")
|
||||
func invariant_notResolvedImpliesNoResult() {
|
||||
let cases: [ScoreResolutionResult] = [
|
||||
.pending,
|
||||
.requiresUserInput(reason: "Test"),
|
||||
.notFound(reason: "Test")
|
||||
]
|
||||
|
||||
for resolution in cases {
|
||||
if !resolution.isResolved {
|
||||
#expect(resolution.result == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ScoreProviderError Tests
|
||||
|
||||
@Suite("ScoreProviderError")
|
||||
struct ScoreProviderErrorTests {
|
||||
|
||||
// MARK: - Specification Tests: errorDescription
|
||||
|
||||
@Test("errorDescription: networkError includes underlying message")
|
||||
func errorDescription_networkError() {
|
||||
let error = ScoreProviderError.networkError(underlying: "Connection timeout")
|
||||
#expect(error.errorDescription?.contains("timeout") == true)
|
||||
}
|
||||
|
||||
@Test("errorDescription: rateLimited has description")
|
||||
func errorDescription_rateLimited() {
|
||||
let error = ScoreProviderError.rateLimited
|
||||
#expect(error.errorDescription != nil)
|
||||
#expect(!error.errorDescription!.isEmpty)
|
||||
}
|
||||
|
||||
@Test("errorDescription: parseError includes message")
|
||||
func errorDescription_parseError() {
|
||||
let error = ScoreProviderError.parseError(message: "Invalid JSON")
|
||||
#expect(error.errorDescription?.contains("Invalid JSON") == true)
|
||||
}
|
||||
|
||||
@Test("errorDescription: gameNotFound has description")
|
||||
func errorDescription_gameNotFound() {
|
||||
let error = ScoreProviderError.gameNotFound
|
||||
#expect(error.errorDescription != nil)
|
||||
}
|
||||
|
||||
@Test("errorDescription: unsupportedSport includes sport")
|
||||
func errorDescription_unsupportedSport() {
|
||||
let error = ScoreProviderError.unsupportedSport(.nfl)
|
||||
#expect(error.errorDescription?.contains("NFL") == true) // rawValue is uppercase
|
||||
}
|
||||
|
||||
@Test("errorDescription: providerUnavailable includes reason")
|
||||
func errorDescription_providerUnavailable() {
|
||||
let error = ScoreProviderError.providerUnavailable(reason: "Maintenance")
|
||||
#expect(error.errorDescription?.contains("Maintenance") == true)
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
/// - Invariant: All errors have non-empty descriptions
|
||||
@Test("Invariant: all errors have descriptions")
|
||||
func invariant_allHaveDescriptions() {
|
||||
let errors: [ScoreProviderError] = [
|
||||
.networkError(underlying: "test"),
|
||||
.rateLimited,
|
||||
.parseError(message: "test"),
|
||||
.gameNotFound,
|
||||
.unsupportedSport(.mlb),
|
||||
.providerUnavailable(reason: "test")
|
||||
]
|
||||
|
||||
for error in errors {
|
||||
#expect(error.errorDescription != nil)
|
||||
#expect(!error.errorDescription!.isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user