Zustandsübergangstests helfen, wenn das Systemverhalten vom aktuellen Zustand eines Objekts abhängt. Benutzer, Bestellungen, Tickets, Zahlungen, Abonnements und Sitzungen besitzen Zustände und zulässige Übergänge.
Wann die Methode verwendet wird
- →Es gibt ausdrückliche Zustände wie erstellt, aktiv, gesperrt, storniert und abgeschlossen.
- →Manche Übergänge sind verboten.
- →Verhalten und verfügbare Aktionen hängen vom aktuellen Zustand ab.
- →Fehler betreffen oft Verletzungen des Lebenszyklus statt einzelner Felder.
Was dokumentiert werden sollte
- →Die Zustände des Objekts
- →Zulässige Übergänge
- →Ereignisse, die Übergänge auslösen
- →Einschränkungen: Wer darf einen Übergang unter welchen Bedingungen auslösen und was folgt daraus?
Beispiel
Ein Fehlerbericht besitzt die Status Open, In Progress, Ready for Test, Reopened und Closed. Zustandsübergangstests prüfen, ob direktes Schließen erlaubt ist, wer ihn wieder öffnen darf und ob ein weiterer Korrekturzyklus die korrekte Logik erhält.
Was QA untersuchen sollte
- →Verbotene Übergänge: Das System sollte sie blockieren oder angemessen erklären.
- →Nebenwirkungen von Übergängen: Benachrichtigungen, Auditierung, Zugriffsänderungen und Protokollierung.
- →Wiederholte Übergänge und Rückwärtsbewegungen.
- →Zustände nach einem Fehler: Was passiert, wenn eine Operation auf halbem Weg unterbrochen wird?
Hat ein Objekt einen Lebenszyklus, ist meist schon ein einfaches Zustandsdiagramm hilfreich. Es zeigt schnell blinde Flecken, die in einer Testfallliste schwer zu erkennen sind.
Ausgearbeiteter Bestelllebenszyklus mit ausdrücklichem Vertrag
Dies ist ein fiktives Modell für eine einzelne Bestellung, nicht das Verhalten jedes Shops oder jeder Zahlungs-API. Draft bedeutet noch nicht abgesendet, Placed abgesendet, Paid bezahlt, Shipped versandt und Cancelled ist ein Endzustand. Nur die folgenden fünf Übergänge sind erlaubt. Stornierung nach Zahlung, Erstattungen, Berechtigungen, Transportfehler und gleichzeitige Anfragen liegen außerhalb dieses kleinen Modells und erfordern zusätzliche Regeln.
Draft --submit--> Placed --pay--> Paid --ship--> Shipped
Draft --cancel--> Cancelled
Placed --cancel--> CancelledTextalternative: submit führt von Draft zu Placed; pay von Placed zu Paid; ship von Paid zu Shipped. cancel führt von Draft oder Placed zu Cancelled. Shipped und Cancelled haben keine ausgehenden Übergänge. Jede andere Kombination aus Zustand und Ereignis wird abgelehnt, ohne den Zustand zu ändern oder eine fachliche Nebenwirkung auszulösen.
Vollständige Testtabelle für einzelne Ereignisse
Die Bestellung vor jeder Zeile auf den Zustand der ersten Spalte zurücksetzen und einen neuen Anfrageschlüssel verwenden. Der Zähler erfasst im Modell einen festgeschriebenen fachlichen Übergang; er steht nicht für eine universelle Anzahl von Protokolleinträgen, Benachrichtigungen oder externen Abbuchungen. Ein akzeptierter Übergang erhöht ihn um eins; eine Ablehnung um null.
Ausgangszustand Ereignis Ergebnis Endzustand Neue Effekte
Draft submit accepted Placed 1
Draft pay rejected Draft 0
Draft ship rejected Draft 0
Draft cancel accepted Cancelled 1
Placed submit rejected Placed 0
Placed pay accepted Paid 1
Placed ship rejected Placed 0
Placed cancel accepted Cancelled 1
Paid submit rejected Paid 0
Paid pay rejected Paid 0
Paid ship accepted Shipped 1
Paid cancel rejected Paid 0
Shipped submit rejected Shipped 0
Shipped pay rejected Shipped 0
Shipped ship rejected Shipped 0
Shipped cancel rejected Shipped 0
Cancelled submit rejected Cancelled 0
Cancelled pay rejected Cancelled 0
Cancelled ship rejected Cancelled 0
Cancelled cancel rejected Cancelled 0Wiederholungen sind ein eigener Vertrag, kein zusätzlicher Zustand
In diesem Modell speichert eine erfolgreiche Anfrage Ereignis und Ergebnis unter einem auf diese Bestellung begrenzten Schlüssel. Dieselbe Kombination aus Schlüssel und Ereignis liefert bei Wiederholung das gespeicherte Ergebnis ohne neue Nebenwirkung. Wird ein erfolgreicher Schlüssel für ein anderes Ereignis verwendet, folgt key-conflict. Abgelehnte Anfragen werden nicht gespeichert. Diese bewusst vereinfachte Regel trifft keine Aussage über einen HTTP-Statuscode oder einen echten Zahlungsanbieter.
Sequenz Erwartete Antwort Aktueller Zustand Gesamtzahl festgeschriebener Effekte
submit / s1 accepted, Placed Placed 1
pay / p1 accepted, Paid Paid 2
pay / p1 erneut accepted, Paid (gespeichertes Ergebnis) Paid 2
ship / p1 key-conflict, Paid Paid 2
pay / p2 rejected, Paid Paid 2
ship / s2 accepted, Shipped Shipped 3Ein gespeichertes Ergebnis ist eine Momentaufnahme der ursprünglichen Anfrage, nicht unbedingt der aktuelle Bestellzustand nach späteren Übergängen. Eine echte API muss Schlüsselbereich, Aufbewahrung, Nutzlastvergleich, Reihenfolge und atomare Speicherung festlegen. Ein reines Arbeitsspeicher-Schulungsmodell kann keinen Schutz vor gleichzeitigen doppelten Abbuchungen nachweisen.
Das Referenzmodell lokal ausführen
Als order_state_model.py speichern und python3 order_state_model.py ausführen. Es prüft alle 20 Zustands-/Ereigniskombinationen und anschließend die Wiederholungssequenz. Erwartete Ausgabe: Transitions: 5 accepted, 15 rejected; replay checks: passed. Diese Prüfungen verifizieren das Schulungsmodell; die Anwendung separat gegen dieselben erklärten Regeln testen.
STATES = ("Draft", "Placed", "Paid", "Shipped", "Cancelled")
EVENTS = ("submit", "pay", "ship", "cancel")
TRANSITIONS = {
("Draft", "submit"): "Placed", ("Draft", "cancel"): "Cancelled",
("Placed", "pay"): "Paid", ("Placed", "cancel"): "Cancelled",
("Paid", "ship"): "Shipped",
}
class Order:
def __init__(self, state="Draft"):
self.state, self.effects, self.saved = state, 0, {}
def apply(self, event, key):
if key in self.saved:
old_event, old_result = self.saved[key]
return old_result if event == old_event else ("key-conflict", self.state)
target = TRANSITIONS.get((self.state, event))
if target is None:
return ("rejected", self.state)
self.state = target
self.effects += 1
result = ("accepted", target)
self.saved[key] = (event, result)
return result
accepted = 0
for state in STATES:
for event in EVENTS:
order = Order(state)
target = TRANSITIONS.get((state, event))
expected = ("accepted", target) if target else ("rejected", state)
assert order.apply(event, "unique") == expected
assert order.state == (target or state)
assert order.effects == int(target is not None)
accepted += target is not None
order = Order()
assert order.apply("submit", "s1") == ("accepted", "Placed")
assert order.apply("pay", "p1") == ("accepted", "Paid")
assert order.apply("pay", "p1") == ("accepted", "Paid")
assert order.effects == 2
assert order.apply("ship", "p1") == ("key-conflict", "Paid")
assert order.apply("pay", "p2") == ("rejected", "Paid")
assert order.effects == 2
assert order.apply("ship", "s2") == ("accepted", "Shipped")
assert order.effects == 3
print(f"Transitions: {accepted} accepted, {20-accepted} rejected; replay checks: passed")Für ein echtes System die Sequenzabdeckung erweitern: Zeitüberschreitung zwischen Zahlung und Speicherung, doppelte Callbacks, veraltete Versionen und gleichzeitige Stornierung und Zahlung. Vor dem Schreiben von Ergebnisprüfungen festlegen, was geschehen soll. Fünf abgedeckte Übergänge beweisen nicht, dass alle mehrstufigen Pfade oder Wiederherstellungen nach Fehlern funktionieren.
Weiterlernen
Entscheidungstabellen für Übergangsbedingungen verwenden
Abgelehnte API-Aktionen auf ausbleibende Nebenwirkungen prüfen