import Testing import Foundation @testable import WeatherProvider // WeatherKit rechnet nach Abrufen ab (500.000 im Monat inklusive). Ein Widget, // das bei jedem Öffnen des Panels neu lädt, verbrennt das Kontingent für Daten, // die sich stündlich ändern. Der Zwischenspeicher ist deshalb kein Feinschliff, // sondern Teil der Funktion. @Suite("Wetter-Zwischenspeicher") struct WeatherCacheTests { private let place = WeatherPlace.fixed(name: "Lemgo", latitude: 52.02, longitude: 8.9) private let other = WeatherPlace.fixed(name: "Berlin", latitude: 52.52, longitude: 13.40) private func snapshot(at date: Date) -> WeatherSnapshot { WeatherSnapshot(placeName: "Lemgo", temperature: .init(value: 18, unit: .celsius), apparentTemperature: .init(value: 17, unit: .celsius), condition: .partlyCloudy, isDaylight: true, humidity: 0.6, windSpeed: .init(value: 10, unit: .kilometersPerHour), high: nil, low: nil, hourly: [], asOf: date, attribution: nil) } @Test("Frischer Eintrag wird wiederverwendet") func freshEntryIsReused() { let now = Date() var cache = WeatherCache() cache.store(snapshot(at: now), for: place) #expect(cache.valid(for: place, now: now.addingTimeInterval(60)) != nil) } @Test("Nach Ablauf der Frist wird neu geladen") func staleEntryIsDiscarded() { let now = Date() var cache = WeatherCache() cache.store(snapshot(at: now), for: place) let later = now.addingTimeInterval(WeatherCache.maxAge + 1) #expect(cache.valid(for: place, now: later) == nil) } @Test("Genau auf der Frist gilt der Eintrag noch") func exactlyAtMaxAgeIsStillValid() { let now = Date() var cache = WeatherCache() cache.store(snapshot(at: now), for: place) #expect(cache.valid(for: place, now: now.addingTimeInterval(WeatherCache.maxAge)) != nil) } @Test("Ein anderer Ort benutzt nicht den Eintrag des ersten") func differentPlaceMisses() { // Sonst zeigt das Widget nach dem Umstellen des Ortes weiter das alte // Wetter — und zwar überzeugend, weil Zahlen dastehen. let now = Date() var cache = WeatherCache() cache.store(snapshot(at: now), for: place) #expect(cache.valid(for: other, now: now) == nil) } @Test("Der aktuelle Standort ist ein eigener Eintrag") func currentLocationIsItsOwnEntry() { let now = Date() var cache = WeatherCache() cache.store(snapshot(at: now), for: .current) #expect(cache.valid(for: .current, now: now) != nil) #expect(cache.valid(for: place, now: now) == nil) } @Test("Leerer Zwischenspeicher liefert nichts") func emptyCacheMisses() { #expect(WeatherCache().valid(for: place, now: Date()) == nil) } @Test("Ein abgelaufener Eintrag bleibt als letzter bekannter Stand erhalten") func staleEntryRemainsAsLastKnown() { // Bei einem Netzfehler ist ein alter Messwert mit sichtbarem Zeitstempel // deutlich besser als ein leeres Feld. let now = Date() var cache = WeatherCache() cache.store(snapshot(at: now), for: place) let later = now.addingTimeInterval(WeatherCache.maxAge * 10) #expect(cache.valid(for: place, now: later) == nil) #expect(cache.lastKnown(for: place) != nil) } @Test("Ein neuer Eintrag ersetzt den alten desselben Ortes") func storingReplaces() { let now = Date() var cache = WeatherCache() cache.store(snapshot(at: now.addingTimeInterval(-1000)), for: place) cache.store(snapshot(at: now), for: place) #expect(cache.lastKnown(for: place)?.asOf == now) } @Test("Die Frist entspricht dem Plan: 15 Minuten") func maxAgeMatchesPlan() { #expect(WeatherCache.maxAge == 15 * 60) } } @Suite("Wettersymbole") struct WeatherSymbolTests { @Test("Klarer Himmel zeigt tagsüber die Sonne, nachts den Mond") func clearDependsOnDaylight() { #expect(WeatherCondition.clear.symbolName(isDaylight: true) == "sun.max.fill") #expect(WeatherCondition.clear.symbolName(isDaylight: false) == "moon.stars.fill") } @Test("Teilweise bewölkt unterscheidet ebenfalls Tag und Nacht") func partlyCloudyDependsOnDaylight() { #expect(WeatherCondition.partlyCloudy.symbolName(isDaylight: true) != WeatherCondition.partlyCloudy.symbolName(isDaylight: false)) } @Test("Regen sieht nachts aus wie tagsüber") func rainIgnoresDaylight() { // Nicht jede Lage hat eine sinnvolle Nachtvariante. Eine erzwungene // Unterscheidung erzeugt nur Symbole, die niemand wiedererkennt. #expect(WeatherCondition.rain.symbolName(isDaylight: true) == WeatherCondition.rain.symbolName(isDaylight: false)) } @Test("Jede Lage liefert ein Symbol — auch die unbekannte") func everyConditionHasASymbol() { for condition in WeatherCondition.allCases { #expect(!condition.symbolName(isDaylight: true).isEmpty) #expect(!condition.symbolName(isDaylight: false).isEmpty) } } }