Files
Sportstime/SportsTimeTests/Services/ScoreResolutionCacheTests.swift
Trey t 8162b4a029 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>
2026-01-16 14:07:41 -06:00

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)
}
}