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>
183 lines
6.3 KiB
Swift
183 lines
6.3 KiB
Swift
//
|
|
// ScoreResolutionCacheTests.swift
|
|
// SportsTimeTests
|
|
//
|
|
// TDD specification tests for ScoreResolutionCache types.
|
|
//
|
|
|
|
import Testing
|
|
import Foundation
|
|
@testable import SportsTime
|
|
|
|
// MARK: - CacheStats Tests
|
|
|
|
@Suite("CacheStats")
|
|
struct CacheStatsTests {
|
|
|
|
// MARK: - Test Data
|
|
|
|
private func makeStats(
|
|
totalEntries: Int = 100,
|
|
entriesWithScores: Int = 80,
|
|
entriesWithoutScores: Int = 20,
|
|
expiredEntries: Int = 5,
|
|
entriesBySport: [Sport: Int] = [.mlb: 50, .nba: 30, .nhl: 20]
|
|
) -> CacheStats {
|
|
CacheStats(
|
|
totalEntries: totalEntries,
|
|
entriesWithScores: entriesWithScores,
|
|
entriesWithoutScores: entriesWithoutScores,
|
|
expiredEntries: expiredEntries,
|
|
entriesBySport: entriesBySport
|
|
)
|
|
}
|
|
|
|
// MARK: - Specification Tests: Properties
|
|
|
|
@Test("CacheStats: stores totalEntries")
|
|
func cacheStats_totalEntries() {
|
|
let stats = makeStats(totalEntries: 150)
|
|
#expect(stats.totalEntries == 150)
|
|
}
|
|
|
|
@Test("CacheStats: stores entriesWithScores")
|
|
func cacheStats_entriesWithScores() {
|
|
let stats = makeStats(entriesWithScores: 75)
|
|
#expect(stats.entriesWithScores == 75)
|
|
}
|
|
|
|
@Test("CacheStats: stores entriesWithoutScores")
|
|
func cacheStats_entriesWithoutScores() {
|
|
let stats = makeStats(entriesWithoutScores: 25)
|
|
#expect(stats.entriesWithoutScores == 25)
|
|
}
|
|
|
|
@Test("CacheStats: stores expiredEntries")
|
|
func cacheStats_expiredEntries() {
|
|
let stats = makeStats(expiredEntries: 10)
|
|
#expect(stats.expiredEntries == 10)
|
|
}
|
|
|
|
@Test("CacheStats: stores entriesBySport")
|
|
func cacheStats_entriesBySport() {
|
|
let bySport: [Sport: Int] = [.mlb: 40, .nba: 60]
|
|
let stats = makeStats(entriesBySport: bySport)
|
|
#expect(stats.entriesBySport[.mlb] == 40)
|
|
#expect(stats.entriesBySport[.nba] == 60)
|
|
}
|
|
|
|
// MARK: - Edge Cases
|
|
|
|
@Test("CacheStats: handles empty cache")
|
|
func cacheStats_emptyCache() {
|
|
let stats = makeStats(
|
|
totalEntries: 0,
|
|
entriesWithScores: 0,
|
|
entriesWithoutScores: 0,
|
|
expiredEntries: 0,
|
|
entriesBySport: [:]
|
|
)
|
|
#expect(stats.totalEntries == 0)
|
|
#expect(stats.entriesBySport.isEmpty)
|
|
}
|
|
|
|
@Test("CacheStats: handles all expired")
|
|
func cacheStats_allExpired() {
|
|
let stats = makeStats(totalEntries: 100, expiredEntries: 100)
|
|
#expect(stats.expiredEntries == stats.totalEntries)
|
|
}
|
|
|
|
@Test("CacheStats: handles all with scores")
|
|
func cacheStats_allWithScores() {
|
|
let stats = makeStats(totalEntries: 100, entriesWithScores: 100, entriesWithoutScores: 0)
|
|
#expect(stats.entriesWithScores == stats.totalEntries)
|
|
#expect(stats.entriesWithoutScores == 0)
|
|
}
|
|
|
|
@Test("CacheStats: handles single sport")
|
|
func cacheStats_singleSport() {
|
|
let stats = makeStats(entriesBySport: [.mlb: 100])
|
|
#expect(stats.entriesBySport.count == 1)
|
|
#expect(stats.entriesBySport[.mlb] == 100)
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: entriesWithScores + entriesWithoutScores == totalEntries
|
|
@Test("Invariant: scores split equals total")
|
|
func invariant_scoresSplitEqualsTotal() {
|
|
let stats = makeStats(totalEntries: 100, entriesWithScores: 80, entriesWithoutScores: 20)
|
|
#expect(stats.entriesWithScores + stats.entriesWithoutScores == stats.totalEntries)
|
|
}
|
|
|
|
/// - Invariant: expiredEntries <= totalEntries
|
|
@Test("Invariant: expired entries cannot exceed total")
|
|
func invariant_expiredCannotExceedTotal() {
|
|
let stats = makeStats(totalEntries: 100, expiredEntries: 50)
|
|
#expect(stats.expiredEntries <= stats.totalEntries)
|
|
}
|
|
|
|
/// - Invariant: sum of entriesBySport <= totalEntries
|
|
@Test("Invariant: sport entries sum does not exceed total")
|
|
func invariant_sportEntriesSumDoesNotExceedTotal() {
|
|
let bySport: [Sport: Int] = [.mlb: 30, .nba: 40, .nhl: 30]
|
|
let stats = makeStats(totalEntries: 100, entriesBySport: bySport)
|
|
let sportSum = bySport.values.reduce(0, +)
|
|
#expect(sportSum <= stats.totalEntries)
|
|
}
|
|
}
|
|
|
|
// MARK: - Cache Expiration Behavior Tests
|
|
|
|
@Suite("Cache Expiration Behavior")
|
|
struct CacheExpirationBehaviorTests {
|
|
|
|
// These tests document the expected cache expiration behavior
|
|
// based on ScoreResolutionCache.calculateExpiration
|
|
|
|
// MARK: - Specification Tests: Cache Durations
|
|
|
|
/// - Expected Behavior: Recent games (< 30 days old) expire after 24 hours
|
|
@Test("Expiration: recent games expire after 24 hours")
|
|
func expiration_recentGames() {
|
|
// Games less than 30 days old should expire after 24 hours
|
|
let recentGameCacheDuration: TimeInterval = 24 * 60 * 60
|
|
#expect(recentGameCacheDuration == 86400) // 24 hours in seconds
|
|
}
|
|
|
|
/// - Expected Behavior: Historical games (> 30 days old) never expire (nil)
|
|
@Test("Expiration: historical games never expire")
|
|
func expiration_historicalGames() {
|
|
// Games older than 30 days should have nil expiration (never expire)
|
|
let historicalAgeThreshold: TimeInterval = 30 * 24 * 60 * 60
|
|
#expect(historicalAgeThreshold == 2592000) // 30 days in seconds
|
|
}
|
|
|
|
/// - Expected Behavior: Failed lookups expire after 7 days
|
|
@Test("Expiration: failed lookups expire after 7 days")
|
|
func expiration_failedLookups() {
|
|
let failedLookupCacheDuration: TimeInterval = 7 * 24 * 60 * 60
|
|
#expect(failedLookupCacheDuration == 604800) // 7 days in seconds
|
|
}
|
|
|
|
// MARK: - Invariant Tests
|
|
|
|
/// - Invariant: Historical threshold > recent cache duration
|
|
@Test("Invariant: historical threshold exceeds recent cache duration")
|
|
func invariant_historicalExceedsRecent() {
|
|
let recentCacheDuration: TimeInterval = 24 * 60 * 60
|
|
let historicalThreshold: TimeInterval = 30 * 24 * 60 * 60
|
|
|
|
#expect(historicalThreshold > recentCacheDuration)
|
|
}
|
|
|
|
/// - Invariant: Failed lookup duration > recent cache duration
|
|
@Test("Invariant: failed lookup duration exceeds recent cache duration")
|
|
func invariant_failedLookupExceedsRecent() {
|
|
let recentCacheDuration: TimeInterval = 24 * 60 * 60
|
|
let failedLookupDuration: TimeInterval = 7 * 24 * 60 * 60
|
|
|
|
#expect(failedLookupDuration > recentCacheDuration)
|
|
}
|
|
}
|