Introdução ao model-based testing para sistemas com estado complexo

1. Fundamentos do Model-Based Testing (MBT)

O Model-Based Testing (MBT) é uma abordagem de teste de software onde casos de teste são gerados automaticamente a partir de modelos formais que descrevem o comportamento esperado do sistema. Diferente dos testes tradicionais — onde o testador manualmente escreve scripts baseados em requisitos — no MBT o foco está na criação e manutenção de um modelo comportamental.

Para sistemas com estado complexo, como plataformas de e-commerce, sistemas de reserva ou jogos, o MBT se destaca porque permite explorar sistematicamente combinações de estados e transições que seriam inviáveis de cobrir manualmente. Enquanto um teste unitário verifica uma função isolada, o MBT verifica sequências de ações que levam o sistema por diferentes estados.

Conceitos-chave:
- Modelo: representação abstrata do comportamento do sistema (ex: máquina de estados finita).
- Estado: configuração específica do sistema em um dado momento.
- Transição: ação ou evento que move o sistema de um estado para outro.
- Caminho de teste: sequência de transições executadas durante um teste.

2. Modelagem de Estados e Transições

A modelagem é o coração do MBT. Utilizamos máquinas de estado finito (FSM) ou diagramas de transição para capturar o comportamento. Para estados complexos, é necessário representar não apenas o estado atual, mas também variáveis de ambiente, histórico de ações e dependências entre componentes.

Exemplo prático: Carrinho de compras

Considere um sistema de carrinho de compras com os seguintes estados:
- vazio
- com_itens
- checkout_iniciado
- pagamento_confirmado
- cancelado

As transições possíveis:
- adicionar_item (vazio → com_itens)
- remover_item (com_itens → vazio, se último item)
- iniciar_checkout (com_itens → checkout_iniciado)
- confirmar_pagamento (checkout_iniciado → pagamento_confirmado)
- cancelar_pedido (checkout_iniciado → cancelado)

Modelamos isso como uma FSM:

Estado: vazio
  Transições: adicionar_item -> com_itens

Estado: com_itens
  Transições: remover_item -> vazio (se ultimo_item)
              iniciar_checkout -> checkout_iniciado

Estado: checkout_iniciado
  Transições: confirmar_pagamento -> pagamento_confirmado
              cancelar_pedido -> cancelado

Estado: pagamento_confirmado
  Transições: (estado final)

Estado: cancelado
  Transições: (estado final)

Para capturar a complexidade real, adicionamos variáveis como quantidade_itens e valor_total. O estado com_itens pode ser subdividido em com_um_item e com_multiplos_itens para testar cenários de remoção parcial.

3. Geração Automática de Casos de Teste a Partir do Modelo

Com o modelo definido, utilizamos ferramentas para gerar caminhos de teste automaticamente. As principais estratégias de geração incluem:

  • Cobertura de estados: garantir que cada estado seja visitado pelo menos uma vez.
  • Cobertura de transições: garantir que cada transição seja executada.
  • Caminhos aleatórios: gerar sequências aleatórias para explorar combinações inesperadas.

Ferramentas populares:
- GraphWalker: open-source, suporta modelos em formato JSON/YAML e gera caminhos baseados em cobertura.
- Spec Explorer: ferramenta da Microsoft para modelagem e geração de testes.
- NModel: biblioteca .NET para MBT.

Exemplo de caminho gerado pelo GraphWalker para o carrinho:

Caminho gerado (cobertura de transições):
1. vazio -> adicionar_item -> com_itens
2. com_itens -> adicionar_item -> com_itens
3. com_itens -> remover_item -> com_itens (restam itens)
4. com_itens -> remover_item -> vazio
5. vazio -> adicionar_item -> com_itens
6. com_itens -> iniciar_checkout -> checkout_iniciado
7. checkout_iniciado -> confirmar_pagamento -> pagamento_confirmado
8. checkout_iniciado -> cancelar_pedido -> cancelado

Para evitar explosão combinatória, filtramos caminhos inviáveis (ex: tentar remover item de carrinho vazio) e limitamos a profundidade máxima.

4. Estratégias para Lidar com a Complexidade de Estado

Sistemas reais podem ter centenas de estados. Três técnicas ajudam a gerenciar essa complexidade:

Particionamento de estados: Divida o modelo em submodelos. Ex: um modelo para o fluxo de compra, outro para o fluxo de devolução.

Abstração: Simplifique estados agrupando configurações similares. Ex: em vez de modelar cada produto individualmente, use categorias (produto_barato, produto_caro).

Redução: Identifique estados equivalentes (ex: dois estados que levam às mesmas transições) e transições redundantes. Pode caminhos que nunca são executados no sistema real.

Exemplo de abstração no carrinho:

Estado original: com_3_itens_total_150
Estado abstraído: com_itens_valor_alto

5. Integração do MBT com o Pipeline de Testes

Os caminhos gerados precisam ser executados contra o sistema real. A integração típica envolve:

  1. Gerar caminhos de teste a partir do modelo.
  2. Converter cada caminho em um script de teste no framework alvo (pytest, JUnit, Robot Framework).
  3. Executar os scripts, verificando pré-condições (estado atual do sistema) e pós-condições (estado esperado após a transição).

Exemplo de executor em Python (pytest):

# executor_mbt.py
import pytest
from sistema_carrinho import Carrinho

caminhos_teste = [
    ["adicionar_item", "adicionar_item", "iniciar_checkout"],
    ["adicionar_item", "remover_item"],
]

@pytest.mark.parametrize("caminho", caminhos_teste)
def test_caminho_carrinho(caminho):
    carrinho = Carrinho()  # estado inicial: vazio
    for acao in caminho:
        if acao == "adicionar_item":
            carrinho.adicionar(produto="livro", preco=50)
            assert carrinho.estado in ["com_itens"]
        elif acao == "remover_item":
            carrinho.remover("livro")
        elif acao == "iniciar_checkout":
            carrinho.checkout()
            assert carrinho.estado == "checkout_iniciado"
    print(f"Caminho {caminho} executado com sucesso")

A sincronização entre modelo e sistema real é crítica: o modelo assume que certas pré-condições são verdadeiras; o executor deve verificar isso antes de cada ação.

6. Validação e Manutenção do Modelo

Um modelo impreciso gera testes falsos. Para garantir fidelidade:

  • Validação contínua: compare o comportamento do modelo com o sistema real usando testes de fumaça.
  • Evolução do modelo: quando o sistema muda (ex: novo estado "reserva_pendente"), atualize o modelo antes de gerar novos testes.
  • Métricas de qualidade: meça a cobertura de estados alcançada pelos testes gerados vs. a cobertura esperada. Um modelo com 80% de cobertura de estados pode indicar caminhos não explorados.

7. Casos de Uso e Limitações do MBT

Quando usar MBT:
- Sistemas com máquina de estados explícita (ex: protocolos de rede, controle de processos).
- Fluxos de trabalho complexos com múltiplas ramificações (ex: sistemas de aprovação).
- Regras de negócio que dependem de histórico de ações.

Desafios comuns:
- Explosão de estados: modelos grandes geram milhões de caminhos. Solução: particionamento e abstração.
- Modelos imprecisos: exigem manutenção constante.
- Custo inicial: modelar um sistema complexo pode levar semanas.

Comparação com outras abordagens:
- Testes baseados em propriedades (ex: Hypothesis): focam em entradas, não em sequências de estados.
- Fuzzing: gera entradas aleatórias, sem modelo comportamental.
- Testes exploratórios: dependem da intuição humana, não de cobertura sistemática.

O MBT brilha onde a ordem das ações importa — exatamente o caso de sistemas com estado complexo.

Referências