Las pruebas de transición de estados ayudan cuando el comportamiento del sistema depende del estado actual de un objeto. Los usuarios, pedidos, incidencias, pagos, suscripciones y sesiones tienen estados y transiciones permitidas.
Cuándo utilizarlas
- →Hay estados explícitos, como creado, activo, bloqueado, cancelado y completado.
- →Algunas transiciones están prohibidas.
- →El comportamiento y las acciones disponibles dependen del estado actual.
- →Los defectos suelen implicar incumplimientos del ciclo de vida, en lugar de campos individuales.
Qué documentar
- →Los estados del objeto
- →Las transiciones permitidas
- →Los eventos que desencadenan las transiciones
- →Las restricciones: quién puede iniciar una transición, en qué condiciones y qué sucede después
Ejemplo
Supongamos que un informe de defecto tiene los estados Open, In Progress, Ready for Test, Reopened y Closed. Las pruebas de transición de estados verifican si se permite el cierre directo, quién puede reabrirlo y si otro ciclo de corrección mantiene la lógica correcta.
Qué debe examinar QA
- →Transiciones prohibidas: el sistema debe bloquearlas o explicarlas de forma adecuada.
- →Efectos secundarios de las transiciones: notificaciones, auditoría, cambios de acceso y registros.
- →Transiciones repetidas y retrocesos.
- →Estados después de un fallo: ¿qué ocurre si una operación se interrumpe a la mitad?
Si un objeto tiene un ciclo de vida, suele ser útil dibujar al menos un diagrama de estados sencillo. Permite detectar rápidamente puntos ciegos difíciles de apreciar en una lista de casos.
Ciclo de vida de un pedido con un contrato explícito
Este es un modelo ficticio de un solo pedido, no el comportamiento de cualquier tienda o API de pagos. Draft es un pedido sin enviar, Placed está presentado, Paid está pagado, Shipped está enviado y Cancelled es terminal. Solo se permiten las cinco transiciones siguientes. La cancelación después del pago, los reembolsos, los permisos, los errores de transporte y las solicitudes concurrentes quedan fuera de este pequeño modelo y requieren reglas adicionales.
Draft --submit--> Placed --pay--> Paid --ship--> Shipped
Draft --cancel--> Cancelled
Placed --cancel--> CancelledVersión textual: submit pasa de Draft a Placed; pay pasa de Placed a Paid; ship pasa de Paid a Shipped. Cancel pasa de Draft o Placed a Cancelled. Shipped y Cancelled no tienen transiciones de salida. Cualquier otra combinación de estado y evento se rechaza sin cambiar el estado ni producir un efecto de negocio.
Tabla completa de pruebas de un solo evento
Antes de cada fila, restablece el pedido al estado de la primera columna y utiliza una clave de solicitud nueva. El contador representa las transiciones de negocio confirmadas en el modelo; no indica un número universal de registros, notificaciones o cargos externos. Una transición aceptada lo incrementa en uno; un rechazo añade cero.
Estado inicial Evento Resultado Estado final Nuevos efectos
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 0Los reintentos son un contrato independiente, no otro estado
En este modelo, una solicitud correcta guarda su evento y su resultado bajo una clave cuyo ámbito es este pedido. Repetir la misma clave y el mismo evento devuelve el resultado guardado sin añadir otro efecto. Reutilizar una clave correcta para un evento diferente devuelve key-conflict. Las solicitudes rechazadas no se guardan. Esta regla deliberadamente simplificada no afirma nada sobre un código HTTP concreto ni sobre un proveedor real de pagos.
Secuencia Respuesta esperada Estado actual Total de efectos confirmados
submit / s1 accepted, Placed Placed 1
pay / p1 accepted, Paid Paid 2
pay / p1 otra vez accepted, Paid (resultado registrado) Paid 2
ship / p1 key-conflict, Paid Paid 2
pay / p2 rejected, Paid Paid 2
ship / s2 accepted, Shipped Shipped 3El resultado guardado es una instantánea de la solicitud original, no necesariamente el estado actual del pedido después de transiciones posteriores. Una API real debe definir el ámbito y la conservación de las claves, la comparación del payload, el orden y la persistencia atómica. Un modelo didáctico que solo utiliza memoria no demuestra protección frente a cargos duplicados concurrentes.
Ejecutar el modelo de referencia localmente
Guárdalo como order_state_model.py y ejecuta python3 order_state_model.py. Comprueba las 20 combinaciones de estado y evento y, después, la secuencia de reintentos. Salida esperada: Transitions: 5 accepted, 15 rejected; replay checks: passed. Estas comprobaciones verifican el modelo didáctico; prueba la aplicación por separado conforme a las mismas reglas declaradas.
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")Para un sistema real, amplía la cobertura de secuencias: tiempo de espera agotado entre el pago y la persistencia, callbacks duplicados, versiones obsoletas y cancelación simultánea al pago. Define qué debería ocurrir antes de escribir las comprobaciones. Cubrir cinco transiciones no demuestra que funcionen todos los recorridos de varios pasos ni todas las recuperaciones de fallos.
Seguir aprendiendo
Utilizar tablas de decisión para las condiciones de transición
Comprobar acciones rechazadas por la API sin efectos secundarios
ISTQB CTFL 4.0.1: sección 4.2.4, pruebas de transición de estados