O teste de transição de estado ajuda quando o comportamento do sistema depende do estado atual de um objeto. Usuários, pedidos, tickets, pagamentos, assinaturas e sessões têm status e transições permitidas.
Quando usar
- →Existem estados explícitos, como criado, ativo, bloqueado, cancelado e concluído.
- →Algumas transições são proibidas.
- →O comportamento e as ações disponíveis dependem do estado atual.
- →Os bugs geralmente envolvem violações do ciclo de vida, em vez de campos individuais.
O que documentar
- →Os estados do objeto
- →Transições permitidas
- →Eventos que desencadeiam transições
- →Restrições: quem pode iniciar uma transição, sob quais condições e o que se segue
Exemplo
Suponha que um relatório de bug tenha os status Aberto, Em Andamento, Pronto para Teste, Reaberto e Fechado. O teste de transição de estado verifica se o fechamento direto é permitido, quem pode reabri-lo e se outro ciclo de correção preserva a lógica correta.
O que QA deve examinar
- →Transições proibidas: o sistema deve bloqueá-las ou explicá-las adequadamente.
- →Efeitos colaterais da transição: notificações, auditoria, alterações de acesso e registro.
- →Transições repetidas e movimento para trás.
- →Estados após falha: o que acontece se uma operação for interrompida no meio?
Se um objeto tiver um ciclo de vida, desenhar até mesmo um diagrama de estado simples geralmente é útil. Ele revela rapidamente pontos cegos que são difíceis de ver em uma lista de casos.
Ciclo de vida de um pedido demonstrado com contrato explícito
Este é um modelo fictício para um único pedido, não o comportamento de toda loja ou API de pagamento. Draft é um pedido não enviado, Placed é enviado, Paid é pago, Shipped é despachado e Cancelled é terminal. Somente as cinco transições abaixo são permitidas. Cancelamento após pagamento, reembolsos, permissões, erros de transporte e requisições concorrentes ficam fora deste modelo pequeno e exigem regras adicionais.
Draft --submit--> Placed --pay--> Paid --ship--> Shipped
Draft --cancel--> Cancelled
Placed --cancel--> CancelledAlternativa em texto: submit move Draft para Placed; pay move Placed para Paid; ship move Paid para Shipped. cancel move Draft ou Placed para Cancelled. Shipped e Cancelled não têm transições de saída. Todas as outras combinações de estado/evento são rejeitadas sem mudar o estado ou produzir um efeito colateral de negócio.
Uma tabela completa de teste de evento único
Restaure o pedido ao estado da primeira coluna antes de cada linha e use uma chave de requisição nova. A contagem é um contador do modelo para uma transição de negócio confirmada; não representa uma quantidade universal de logs, notificações ou cobranças externas. Uma transição aceita incrementa o contador em um; uma rejeição acrescenta zero.
Estado inicial Evento Resultado Estado final Novos efeitos
Draft submit aceito Placed 1
Draft pay rejeitado Draft 0
Draft ship rejeitado Draft 0
Draft cancel aceito Cancelled 1
Placed submit rejeitado Placed 0
Placed pay aceito Paid 1
Placed ship rejeitado Placed 0
Placed cancel aceito Cancelled 1
Paid submit rejeitado Paid 0
Paid pay rejeitado Paid 0
Paid ship aceito Shipped 1
Paid cancel rejeitado Paid 0
Shipped submit rejeitado Shipped 0
Shipped pay rejeitado Shipped 0
Shipped ship rejeitado Shipped 0
Shipped cancel rejeitado Shipped 0
Cancelled submit rejeitado Cancelled 0
Cancelled pay rejeitado Cancelled 0
Cancelled ship rejeitado Cancelled 0
Cancelled cancel rejeitado Cancelled 0As novas tentativas são um contrato separado, não um estado extra
Neste modelo, uma requisição bem-sucedida armazena seu evento e resultado sob uma chave limitada a este pedido. Repetir a mesma chave e evento retorna o resultado armazenado sem novo efeito colateral. Reutilizar uma chave bem-sucedida para outro evento retorna key-conflict. Requisições rejeitadas não são armazenadas. Essa regra intencionalmente simplificada não afirma nada sobre códigos de status HTTP ou provedores reais de pagamento.
Sequência Resposta esperada Estado atual Total de efeitos confirmados
submit / s1 aceito, Placed Placed 1
pay / p1 aceito, Paid Paid 2
pay / p1 novamente aceito, Paid (resultado registrado) Paid 2
ship / p1 key-conflict, Paid Paid 2
pay / p2 rejeitado, Paid Paid 2
ship / s2 aceito, Shipped Shipped 3Um resultado armazenado é um snapshot da requisição original, não necessariamente o estado atual do pedido após transições posteriores. Uma API real deve especificar escopo da chave, retenção, comparação de payload, ordenação e persistência atômica. Um modelo didático apenas em memória não demonstra proteção contra cobranças duplicadas concorrentes.
Execute o modelo de referência localmente
Salve como order_state_model.py e execute python3 order_state_model.py. Ele verifica todas as 20 combinações de estado/evento e depois a sequência de novas tentativas. Saída esperada: Transitions: 5 accepted, 15 rejected; replay checks: passed. Essas verificações validam este modelo didático; teste a aplicação separadamente conforme as mesmas regras 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")Em um sistema real, amplie a cobertura de sequências: timeout entre pagamento e persistência, callbacks duplicados, versões antigas e cancelamento concorrente com pagamento. Defina o que deve acontecer antes de escrever as asserções. Cinco transições cobertas não provam que todos os caminhos de várias etapas ou recuperações de falhas funcionem.
Continuar aprendendo
Use tabelas de decisão para as condições de guarda das transições