0%

Abschlussprojekt: Ein Werkzeug für Übungsberichte · Übung

Aktualität braucht einen übergebenen Zeitpunkt

Der Cache sagt jetzt, wann er abgerufen wurde. Zu entscheiden, ob das kürzlich genug war, klingt nach einem Vergleich in einer Zeile. Fast stimmt das auch. Interessant ist, woher die aktuelle Zeit kommt.

Die naheliegende Variante und ihre Schwierigkeiten

Das schreiben die meisten zuerst:

def cache_is_stale(fetched_at, max_age):
    return int(time.time()) - fetched_at > max_age

Es funktioniert. Versuche jetzt, es zu testen.

Du möchtest prüfen, dass ein Cache eine Sekunde jenseits seiner Grenze als veraltet gilt. Dafür muss time.time() einen bestimmten Wert zurückgeben. Es gibt aber jetzt zurück, und jetzt bewegt sich. Du könntest den Cache mit einem von der aktuellen Zeit rückwärts berechneten Zeitstempel schreiben. Dann rechnet dein Test jedoch, um eine Uhr auszugleichen. Jeder Test, der etwas ausgleichen muss, kann abdriften.

Du könntest den Test schlafen lassen. Jetzt ist deine Suite absichtlich langsamer und immer noch nicht exakt.

Die Funktion ist nicht schwer zu testen, weil Testen schwer wäre. Sie ist schwer zu testen, weil sie sich eigenständig einen Wert holt, den ihr niemand übergeben hat.

Lass dir die Zeit stattdessen geben

Mache die aktuelle Zeit zu einem Parameter:

def cache_is_stale(fetched_at, now_epoch, max_age):
    ...

Das ist die ganze Technik. Sie heißt Dependency Injection, also Übergabe von Abhängigkeiten. Das klingt viel größer, als es ist: Lass dir etwas übergeben, statt es selbst zu holen.

Jetzt sagt ein Test genau, was er meint. Der Cache wurde bei 100 geschrieben, jetzt ist 110, und die Grenze beträgt 10 Sekunden. Ist er veraltet? Nein: 110 minus 100 ist 10, und das ist nicht mehr als 10. Und bei 111? Ja, um eine Sekunde. Beide Fälle laufen sofort und liefern dauerhaft dieselbe Antwort.

Wo die Uhr tatsächlich gelesen wird

Irgendetwas muss schließlich time.time() aufrufen. Die Regel lautet: einmal, weit oben, in der Ebene, die den Ablauf steuert:

if now_epoch is None:
    now_epoch = int(time.time())

Die steuernde Funktion liest die Uhr einmal, wenn kein Zeitpunkt übergeben wurde, und reicht diesen einen Wert an alles Weitere weiter. Tests übergeben now_epoch, und die Uhr wird nie berührt.

Einmal zu lesen ist wichtiger, als es aussieht. Wenn drei verschiedene Funktionen jeweils time.time() aufrufen, erhalten sie drei leicht unterschiedliche Antworten. Ein Programm kann dann im selben Lauf an einer Stelle den Cache für aktuell und an einer anderen für veraltet halten. Ein Lesevorgang, ein Wert, nach unten weitergereicht.

Lege die Grenze ausdrücklich fest

„Eine Stunde lang aktuell“ hat einen Grenzfall, und unklarer Code entscheidet stillschweigend darüber. Sprich die Entscheidung stattdessen aus:

  • Alter kleiner als max_age: aktuell

  • Alter genau gleich max_age: aktuell

  • Alter eine Sekunde größer als max_age: veraltet

Der Vergleich lautet also age > max_age, nicht >=. Das ist dieselbe Entscheidung zwischen > und >=, deren Fehler du in Kapitel 9 gesucht hast. Daran siehst du, wie oft sie vorkommt.

Ein Zeitstempel aus der Zukunft bedeutet nicht aktuell

Ein Fall bleibt übrig, und die verlockende Antwort ist falsch.

Was passiert, wenn fetched_at später als now_epoch liegt? Das Alter ist negativ. Negativ ist auf jeden Fall kleiner als max_age, also nennt ein naiver Vergleich den Cache aktuell. Ein Cache mit ungültigem Zeitstempel könnte so bis zu diesem zukünftigen Zeitpunkt plus max_age verwendet werden.

Ein zukünftiger Zeitstempel bedeutet keine aktuellen Daten. Er bedeutet kaputte Daten: Eine Uhr wurde verändert, eine Datei bearbeitet, etwas ist schiefgegangen. Löse ValueError aus und lass den Aufrufer den Cache als ungültig behandeln und erneut abrufen.

Alle drei Werte müssen echte nicht negative ganze Zahlen sein, geprüft mit type(value) is int aus der vorigen Lektion. Für isinstance gilt True schließlich weiterhin als ganze Zahl.

Jetzt bist du dran

Implementiere cache_is_stale(fetched_at, now_epoch, max_age) in practice_report.py.

Gib für aktuell False und für veraltet True zurück. Löse ValueError aus, wenn ein Wert keine nicht negative ganze Zahl ist oder wenn fetched_at in der Zukunft liegt.

Rufe darin nicht time.time() auf. Sieh dir keine Änderungszeiten von Dateien an. Die drei übergebenen Zahlen sind die gesamte Eingabe.

Warum erhält cache_is_stale den Parameter now_epoch, statt time.time() aufzurufen?

Ein Cache behauptet, er sei in 60 Sekunden abgerufen worden. Was sollte cache_is_stale tun?

Warum liest die Ablaufsteuerung die Uhr einmal und reicht den Wert weiter?

Aufgabe

Ergänze die genau vorgegebene Aktualitätsprüfung mit übergebener Zeit und decke Epochenzeit null, genau die Altersgrenze, eine Sekunde darüber, zukünftige Zeit und problematische Boolean-Werte ohne Schlafpausen ab.