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:
268
SportsTimeTests/Services/AchievementEngineTests.swift
Normal file
268
SportsTimeTests/Services/AchievementEngineTests.swift
Normal file
@@ -0,0 +1,268 @@
|
||||
//
|
||||
// AchievementEngineTests.swift
|
||||
// SportsTimeTests
|
||||
//
|
||||
// TDD specification tests for AchievementEngine types.
|
||||
//
|
||||
|
||||
import Testing
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
@testable import SportsTime
|
||||
|
||||
// MARK: - AchievementDelta Tests
|
||||
|
||||
@Suite("AchievementDelta")
|
||||
struct AchievementDeltaTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private func makeDefinition(id: String = "test_achievement") -> AchievementDefinition {
|
||||
AchievementDefinition(
|
||||
id: id,
|
||||
name: "Test Achievement",
|
||||
description: "Test description",
|
||||
category: .count,
|
||||
sport: nil,
|
||||
iconName: "star.fill",
|
||||
iconColor: .blue,
|
||||
requirement: .firstVisit
|
||||
)
|
||||
}
|
||||
|
||||
private func makeDelta(
|
||||
newlyEarned: [AchievementDefinition] = [],
|
||||
revoked: [AchievementDefinition] = [],
|
||||
stillEarned: [AchievementDefinition] = []
|
||||
) -> AchievementDelta {
|
||||
AchievementDelta(
|
||||
newlyEarned: newlyEarned,
|
||||
revoked: revoked,
|
||||
stillEarned: stillEarned
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: hasChanges
|
||||
|
||||
/// - Expected Behavior: true when newlyEarned is not empty
|
||||
@Test("hasChanges: true when newlyEarned not empty")
|
||||
func hasChanges_newlyEarned() {
|
||||
let delta = makeDelta(newlyEarned: [makeDefinition()])
|
||||
#expect(delta.hasChanges == true)
|
||||
}
|
||||
|
||||
/// - Expected Behavior: true when revoked is not empty
|
||||
@Test("hasChanges: true when revoked not empty")
|
||||
func hasChanges_revoked() {
|
||||
let delta = makeDelta(revoked: [makeDefinition()])
|
||||
#expect(delta.hasChanges == true)
|
||||
}
|
||||
|
||||
/// - Expected Behavior: false when both are empty (stillEarned doesn't count)
|
||||
@Test("hasChanges: false when newlyEarned and revoked both empty")
|
||||
func hasChanges_bothEmpty() {
|
||||
let delta = makeDelta(newlyEarned: [], revoked: [], stillEarned: [makeDefinition()])
|
||||
#expect(delta.hasChanges == false)
|
||||
}
|
||||
|
||||
/// - Expected Behavior: true when both newlyEarned and revoked have items
|
||||
@Test("hasChanges: true when both newlyEarned and revoked have items")
|
||||
func hasChanges_bothHaveItems() {
|
||||
let delta = makeDelta(
|
||||
newlyEarned: [makeDefinition(id: "new")],
|
||||
revoked: [makeDefinition(id: "old")]
|
||||
)
|
||||
#expect(delta.hasChanges == true)
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
/// - Invariant: hasChanges == (!newlyEarned.isEmpty || !revoked.isEmpty)
|
||||
@Test("Invariant: hasChanges formula is correct")
|
||||
func invariant_hasChangesFormula() {
|
||||
let testCases: [(newlyEarned: [AchievementDefinition], revoked: [AchievementDefinition])] = [
|
||||
([], []),
|
||||
([makeDefinition()], []),
|
||||
([], [makeDefinition()]),
|
||||
([makeDefinition()], [makeDefinition()])
|
||||
]
|
||||
|
||||
for (newlyEarned, revoked) in testCases {
|
||||
let delta = makeDelta(newlyEarned: newlyEarned, revoked: revoked)
|
||||
let expected = !newlyEarned.isEmpty || !revoked.isEmpty
|
||||
#expect(delta.hasChanges == expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AchievementProgress Tests
|
||||
|
||||
@Suite("AchievementProgress")
|
||||
struct AchievementProgressTests {
|
||||
|
||||
// MARK: - Test Data
|
||||
|
||||
private func makeDefinition() -> AchievementDefinition {
|
||||
AchievementDefinition(
|
||||
id: "test_progress",
|
||||
name: "Test Progress",
|
||||
description: "Test",
|
||||
category: .count,
|
||||
sport: nil,
|
||||
iconName: "star",
|
||||
iconColor: .blue,
|
||||
requirement: .visitCount(10)
|
||||
)
|
||||
}
|
||||
|
||||
private func makeProgress(
|
||||
currentProgress: Int = 5,
|
||||
totalRequired: Int = 10,
|
||||
hasStoredAchievement: Bool = false,
|
||||
earnedAt: Date? = nil
|
||||
) -> AchievementProgress {
|
||||
AchievementProgress(
|
||||
definition: makeDefinition(),
|
||||
currentProgress: currentProgress,
|
||||
totalRequired: totalRequired,
|
||||
hasStoredAchievement: hasStoredAchievement,
|
||||
earnedAt: earnedAt
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: isEarned
|
||||
|
||||
/// - Expected Behavior: true when hasStoredAchievement is true
|
||||
@Test("isEarned: true when hasStoredAchievement")
|
||||
func isEarned_hasStoredAchievement() {
|
||||
let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: true)
|
||||
#expect(progress.isEarned == true)
|
||||
}
|
||||
|
||||
/// - Expected Behavior: true when progress >= total (and total > 0)
|
||||
@Test("isEarned: true when progress equals total")
|
||||
func isEarned_progressEqualsTotal() {
|
||||
let progress = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: false)
|
||||
#expect(progress.isEarned == true)
|
||||
}
|
||||
|
||||
/// - Expected Behavior: true when progress > total
|
||||
@Test("isEarned: true when progress exceeds total")
|
||||
func isEarned_progressExceedsTotal() {
|
||||
let progress = makeProgress(currentProgress: 15, totalRequired: 10, hasStoredAchievement: false)
|
||||
#expect(progress.isEarned == true)
|
||||
}
|
||||
|
||||
/// - Expected Behavior: false when progress < total and no stored achievement
|
||||
@Test("isEarned: false when progress less than total")
|
||||
func isEarned_progressLessThanTotal() {
|
||||
let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: false)
|
||||
#expect(progress.isEarned == false)
|
||||
}
|
||||
|
||||
/// - Expected Behavior: false when total is 0 (edge case)
|
||||
@Test("isEarned: false when total is 0")
|
||||
func isEarned_totalZero() {
|
||||
let progress = makeProgress(currentProgress: 5, totalRequired: 0, hasStoredAchievement: false)
|
||||
#expect(progress.isEarned == false)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: progressPercentage
|
||||
|
||||
/// - Expected Behavior: Returns current/total as Double
|
||||
@Test("progressPercentage: returns correct ratio")
|
||||
func progressPercentage_correctRatio() {
|
||||
let progress = makeProgress(currentProgress: 5, totalRequired: 10)
|
||||
#expect(progress.progressPercentage == 0.5)
|
||||
}
|
||||
|
||||
@Test("progressPercentage: 100% when complete")
|
||||
func progressPercentage_complete() {
|
||||
let progress = makeProgress(currentProgress: 10, totalRequired: 10)
|
||||
#expect(progress.progressPercentage == 1.0)
|
||||
}
|
||||
|
||||
@Test("progressPercentage: 0% when no progress")
|
||||
func progressPercentage_noProgress() {
|
||||
let progress = makeProgress(currentProgress: 0, totalRequired: 10)
|
||||
#expect(progress.progressPercentage == 0.0)
|
||||
}
|
||||
|
||||
@Test("progressPercentage: 0 when total is 0")
|
||||
func progressPercentage_totalZero() {
|
||||
let progress = makeProgress(currentProgress: 5, totalRequired: 0)
|
||||
#expect(progress.progressPercentage == 0.0)
|
||||
}
|
||||
|
||||
@Test("progressPercentage: can exceed 100%")
|
||||
func progressPercentage_exceedsHundred() {
|
||||
let progress = makeProgress(currentProgress: 15, totalRequired: 10)
|
||||
#expect(progress.progressPercentage == 1.5)
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: progressText
|
||||
|
||||
/// - Expected Behavior: "Completed" when earned
|
||||
@Test("progressText: Completed when earned via stored")
|
||||
func progressText_completedViaStored() {
|
||||
let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: true)
|
||||
#expect(progress.progressText == "Completed")
|
||||
}
|
||||
|
||||
@Test("progressText: Completed when earned via progress")
|
||||
func progressText_completedViaProgress() {
|
||||
let progress = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: false)
|
||||
#expect(progress.progressText == "Completed")
|
||||
}
|
||||
|
||||
/// - Expected Behavior: "current/total" when not earned
|
||||
@Test("progressText: shows fraction when not earned")
|
||||
func progressText_showsFraction() {
|
||||
let progress = makeProgress(currentProgress: 5, totalRequired: 10, hasStoredAchievement: false)
|
||||
#expect(progress.progressText == "5/10")
|
||||
}
|
||||
|
||||
@Test("progressText: shows 0/total when no progress")
|
||||
func progressText_zeroProgress() {
|
||||
let progress = makeProgress(currentProgress: 0, totalRequired: 10, hasStoredAchievement: false)
|
||||
#expect(progress.progressText == "0/10")
|
||||
}
|
||||
|
||||
// MARK: - Specification Tests: id
|
||||
|
||||
/// - Expected Behavior: id returns definition.id
|
||||
@Test("id: returns definition id")
|
||||
func id_returnsDefinitionId() {
|
||||
let progress = makeProgress()
|
||||
#expect(progress.id == "test_progress")
|
||||
}
|
||||
|
||||
// MARK: - Invariant Tests
|
||||
|
||||
/// - Invariant: progressPercentage == Double(current) / Double(total) when total > 0
|
||||
@Test("Invariant: progressPercentage calculation correct")
|
||||
func invariant_progressPercentageFormula() {
|
||||
let testCases: [(current: Int, total: Int)] = [
|
||||
(0, 10),
|
||||
(5, 10),
|
||||
(10, 10),
|
||||
(15, 10),
|
||||
(1, 3)
|
||||
]
|
||||
|
||||
for (current, total) in testCases {
|
||||
let progress = makeProgress(currentProgress: current, totalRequired: total)
|
||||
let expected = Double(current) / Double(total)
|
||||
#expect(abs(progress.progressPercentage - expected) < 0.0001)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Invariant: isEarned implies progressText == "Completed"
|
||||
@Test("Invariant: isEarned implies Completed text")
|
||||
func invariant_earnedImpliesCompletedText() {
|
||||
let earned = makeProgress(currentProgress: 10, totalRequired: 10, hasStoredAchievement: true)
|
||||
if earned.isEarned {
|
||||
#expect(earned.progressText == "Completed")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user