Les tests de transition d'état sont utiles lorsque le comportement du système dépend de l'état actuel d'un objet. Les utilisateurs, les commandes, les tickets, les paiements, les abonnements et les sessions ont tous des statuts et des transitions autorisées.
Quand l'utiliser
- →Il existe des états explicites tels que créé, actif, bloqué, annulé et terminé.
- →Certaines transitions sont interdites.
- →Le comportement et les actions disponibles dépendent de l'état actuel.
- →Les bogues impliquent souvent des violations du cycle de vie plutôt que des champs individuels.
Que documenter
- →Les états de l'objet
- →Transitions autorisées
- →Événements qui déclenchent des transitions
- →Restrictions : qui peut initier une transition, dans quelles conditions et ce qui suit
Exemple
Supposons qu'un rapport de bogue ait les statuts Ouvert, En cours, Prêt pour le test, Réouvert et Fermé. Les tests de transition d'état vérifient si la fermeture directe est autorisée, qui peut la rouvrir et si un autre cycle de correctifs préserve la logique correcte.
Ce que QA devrait examiner
- →Transitions interdites : le système doit les bloquer ou les expliquer de manière appropriée.
- →Effets secondaires de la transition : notifications, audit, modifications d'accès et journalisation.
- →Transitions répétées et mouvement vers l’arrière.
- →États après échec : que se passe-t-il si une opération est interrompue à mi-chemin ?
Si un objet a un cycle de vie, dessiner même un simple diagramme d’état est généralement utile. Il révèle rapidement des angles morts difficiles à déceler dans une liste de cas.
Exemple de cycle de vie d’une commande avec un contrat explicite
Ce modèle fictif porte sur une seule commande ; il ne décrit pas toutes les boutiques ni toutes les API de paiement. Draft désigne une commande non envoyée, Placed une commande soumise, Paid une commande payée, Shipped une commande expédiée et Cancelled un état terminal. Seules les cinq transitions ci-dessous sont autorisées. Annulations après paiement, remboursements, autorisations, erreurs de transport et requêtes concurrentes exigent des règles supplémentaires.
Draft --submit--> Placed --pay--> Paid --ship--> Shipped
Draft --cancel--> Cancelled
Placed --cancel--> CancelledVersion textuelle : submit fait passer de Draft à Placed ; pay de Placed à Paid ; ship de Paid à Shipped. cancel fait passer de Draft ou Placed à Cancelled. Shipped et Cancelled n’ont aucune transition sortante. Toute autre combinaison état/événement est refusée sans changement d’état ni effet métier.
Une table de test complète à événement unique
Avant chaque ligne, remettez la commande dans l’état de la première colonne et utilisez une nouvelle clé de requête. Le compteur mesure les transitions métier enregistrées dans ce modèle ; il ne représente pas un nombre universel de journaux, notifications ou paiements externes. Une transition acceptée l’incrémente de un ; un refus n’ajoute rien.
État initial Événement Résultat État final Nouveaux effets
Draft submit accepté Placed 1
Draft pay refusé Draft 0
Draft ship refusé Draft 0
Draft cancel accepté Cancelled 1
Placed submit refusé Placed 0
Placed pay accepté Paid 1
Placed ship refusé Placed 0
Placed cancel accepté Cancelled 1
Paid submit refusé Paid 0
Paid pay refusé Paid 0
Paid ship accepté Shipped 1
Paid cancel refusé Paid 0
Shipped submit refusé Shipped 0
Shipped pay refusé Shipped 0
Shipped ship refusé Shipped 0
Shipped cancel refusé Shipped 0
Cancelled submit refusé Cancelled 0
Cancelled pay refusé Cancelled 0
Cancelled ship refusé Cancelled 0
Cancelled cancel refusé Cancelled 0Les nouvelles tentatives relèvent d’un contrat distinct, pas d’un état supplémentaire
Pour ce modèle, une requête réussie stocke son événement et son résultat sous une clé limitée à cette commande. La répétition de la même clé et du même événement renvoie le résultat stocké sans nouvel effet secondaire. La réutilisation d'une clé réussie pour un événement différent renvoie un conflit de clés. Les demandes rejetées ne sont pas stockées. Cette règle intentionnellement simplifiée ne constitue pas une affirmation concernant un code de statut HTTP ou un véritable fournisseur de paiement.
Séquence Réponse attendue État actuel Total des effets enregistrés
submit / s1 accepté, Placed Placed 1
pay / p1 accepté, Paid Paid 2
pay / p1 à nouveau accepté, Paid (résultat enregistré) Paid 2
ship / p1 key-conflict, Paid Paid 2
pay / p2 refusé, Paid Paid 2
ship / s2 accepté, Shipped Shipped 3Un résultat mémorisé représente la requête initiale, pas forcément l’état actuel de la commande après des transitions ultérieures. Une vraie API doit préciser la portée et la durée de conservation des clés, la comparaison des données, l’ordre des opérations et la persistance atomique. Ce modèle pédagogique en mémoire ne démontre pas une protection contre des débits simultanés en double.
Exécuter le modèle de référence localement
Enregistrez le code dans order_state_model.py et exécutez python3 order_state_model.py. Il vérifie les 20 combinaisons état/événement, puis la séquence de nouvelles tentatives. Sortie attendue : Transitions: 5 accepted, 15 rejected; replay checks: passed. Ces vérifications portent sur le modèle pédagogique ; testez séparément l’application selon les mêmes règles déclarées.
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")Pour un vrai système, étendez la couverture des séquences : délai d'attente entre le paiement et la persistance, rappels en double, versions obsolètes et course aux annulations avec paiement. Définissez ce qui doit se passer avant d’écrire les assertions. Cinq transitions couvertes ne prouvent pas que tous les chemins en plusieurs étapes ou les récupérations après échec fonctionnent.
Pour aller plus loin
Utiliser des tables de décision pour les gardes de transition
Vérifiez les actions API rejetées sans effets secondaires
ISTQB CTFL 4.0.1 : section 4.2.4, tests de transition d'état