Zustandsübergangstests

Wie Systeme getestet werden, deren Verhalten vom Objektstatus abhängt, und wie Fehler in der Workflow-Logik gefunden werden.

Aktualisiert:

Inhalt

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.

plain text
Draft --submit--> Placed --pay--> Paid --ship--> Shipped
Draft --cancel--> Cancelled
Placed --cancel--> Cancelled

Textalternative: 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.

plain text
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	0

Wiederholungen 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.

plain text
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	3

Ein 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.

plain text
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

ISTQB CTFL 4.0.1: Abschnitt 4.2.4, Zustandsübergangstests