Teste baseado em modelo

Introdução ao teste baseado em modelo e Spec Explorer

Sergio Mera
Yiming Cao

Produzir software de alta qualidade demanda um esforço significativo em teste, que provavelmente é uma das partes mais caras e intensas do processo de desenvolvimento de software. Tem havido muitas abordagens para melhoria da confiabilidade e eficiência dos testes, dos mais simples testes funcionais de caixa preta a métodos pesados envolvendo demonstrações de teoremas e especificações de requisitos formais. No entanto, o teste nem sempre inclui o nível necessário de abrangência, e a disciplina e a metodologia estão frequentemente ausentes.

A Microsoft tem aplicado com sucesso o teste baseado em modelo (MBT) ao seu processo de desenvolvimento interno já há mais de uma década. O MBT provou ser uma técnica bem-sucedida para uma variedade de produtos de software internos e externos. Sua adoção aumentou de forma constante ao longo dos anos. Falando de forma relativa, ele foi bem recebido na comunidade de teste, particularmente quando comparado com outras metodologias presentes no lado “formal” do espectro de testes.

O Spec Explorer é uma ferramenta de MBT da Microsoft que amplia o Visual Studio, fornecendo um ambiente de desenvolvimento altamente integrado para criação de modelos comportamentais, mais uma ferramenta de análise gráfica para verificação da validade desses modelos e geração de casos de teste a partir deles. Acreditamos que essa ferramenta foi o ponto principal que facilitou a aplicação do MBT como uma técnica eficaz no setor de TI, atenuando a curva de aprendizado natural e fornecendo um ambiente de criação avançado.

Neste artigo, fornecemos uma visão geral dos principais conceitos por trás do MBT e do Spec Explorer, apresentando o Spec Explorer por meio de um estudo de caso para demonstrar seus recursos principais. Queremos também que esse artigo sirva com uma coleção de regras básicas práticas para entender quando considerar o MBT como uma metodologia de controle de qualidade para um problema de teste específico. Você não deve usar cegamente o MBT em todos os cenários de teste. Muitas vezes, outra técnica (como o teste tradicional) pode ser uma melhor opção.

O que faz um modelo possível de ser testado no Spec Explorer?

Embora diferentes ferramentas de MBT ofereçam diferentes funcionalidades e algumas vezes tenham pequenas discrepâncias conceituais, há uma concordância geral sobre o significado de “executar MBT”. O teste baseado em modelo diz respeito à geração automática de procedimentos de teste a partir de modelos.

Geralmente, os modelos são criados manualmente e incluem requisitos do sistema e comportamento esperado. No caso do Spec Explorer, os casos de teste são gerados automaticamente de um modelo orientado por estado. Eles incluem as sequências de teste e o oracle de teste. As sequências de teste, inferidas do modelo, são responsáveis por conduzir o sistema em teste (SUT) para alcançar diferentes estados. O oracle de teste acompanha a evolução do SUT e determina se ele está em conformidade com o comportamento especificado pelo modelo, emitindo um veredito.

O modelo é um das partes principais em um projeto Spec Explorer. Ele é especificado em um constructo chamado programas de modelo. É possível escrever programas de modelo em qualquer linguagem .NET (tal como C#). Eles consistem em um conjunto de regras que interagem com um estado definido. Os programas de modelo são combinados com uma linguagem de scripts chamada Cord, a segunda parte principal em um projeto Spec Explorer. Isso permite especificar as descrições comportamentais que configuram como o modelo é explorado e testado. A combinação do programa de modelo e o script Cord cria um modelo de teste do SUT.

Certamente, a terceira parte importante no projeto Spec Explorer é o SUT. Não é mandatório fornecer isso ao Spec Explorer para gerar o código de teste (que é o modo padrão do Spec Explorer), pois o código gerado é inferido diretamente do modelo de teste, sem nenhuma interação com o SUT. É possível executar casos de teste “offline”, dissociado dos estágios de avaliação do modelo e geração de casos de teste. No entanto, se o SUT for fornecido, o Spec Explorer poderá validar se as associações do modelo para a implementação estão bem-definidas.

Estudo de caso: um sistema de chat

Vamos dar uma olhada em um exemplo para mostrar como você pode criar um modelo de teste no Spec Explorer. O SUT nesse caso será um simples sistema de chat com uma única sala de chat em que os usuários podem fazer logon e logoff. Quando um usuário está conectado, ele pode solicitar a lista de usuários conectados e enviar mensagens de difusão a todos. O servidor de chat sempre confirma as solicitações. Solicitações e respostas comportam-se de forma assíncrona, significando que podem ser misturadas. No entanto, como esperado em um sistema de chat, múltiplas mensagens enviadas de um usuário são recebidas em ordem.

Uma das vantagens de se usar MBT é que, com a imposição da necessidade de formalizar o modelo comportamental, é possível obter muitos comentários para os requisitos. Ambiguidade, contradições e falta de contexto podem surgir nos estágios iniciais. Portanto, é importante ser preciso e formalizar os requisitos do sistema, como a seguir:

R1. Users must receive a response for a logon request.
R2. Users must receive a response for a logoff request.
R3. Users must receive a response for a list request.
R4. List response must contain the list of logged-on users.
R5. All logged-on users must receive a broadcast message.
R6. Messages from one sender must be received in order.

Os projetos Spec Explorer usam ações para descrever a interação com o SUT do ponto de vista do sistema de teste. Essas ações podem ser ações de chamada, representando um estímulo do sistema de teste para o SUT; ações de retorno, capturando a resposta do SUT (se houver); e ações de evento, representando mensagens autônomas enviadas do SUT. Ações de chamada/retorno são operações de bloqueio, portanto, são representadas por um método único no SUT. Essas são as declarações de ação padrão, enquanto a palavra-chave “event” é usada para declarar uma ação de evento. A Figura 1 mostra como fica isso no sistema de chat.

Figura 1 Declarações de ação

// Cord code
config ChatConfig
{
  action void LogonRequest(int user);
  action event void LogonResponse(int user);
  action void LogoffRequest(int user);
  action event void LogoffResponse(int user);
  action void ListRequest(int user);
  action event void ListResponse(int user, Set<int> userList);
  action void BroadcastRequest(int senderUser, string message);
  action void BroadcastAck(int receiverUser, 
    int senderUser, string message);
  // ...
}

Com as ações declaradas, a próxima etapa é definir o comportamento do sistema. Para esse exemplo, o modelo está descrito usando C#. O estado do sistema é modelado com campos de classe, e transições de estado são modeladas com métodos de regra. Os métodos de regra determinam as etapas que podem ser tomadas a partir do estado atual no programa de modelo, e como o estado é atualizado para cada etapa.

Como esse sistema de chat consiste essencialmente na interação entre usuários e o sistema, o estado do modelo é simplesmente uma coleção de usuários com seus estados (consulte a Figura 2).

Figura 2 O estado do modelo

/// <summary>
/// A model of the MS-CHAT sample.
/// </summary>
public static class Model
{
  /// <summary>
  /// State of the user.
  /// </summary>
  enum UserState
  {
    WaitingForLogon,
    LoggedOn,
    WaitingForList,
    WatingForLogoff,
  }
  /// <summary>
  /// A class representing a user
  /// </summary>
  partial class User
  {
    /// <summary>
    /// The state in which the user currently is.
    /// </summary>
    internal UserState state;
    /// <summary>
    /// The broadcast messages that are waiting for delivery to this user.
    /// This is a map indexed by the user who broadcasted the message,
    /// mapping into a sequence of broadcast messages from this same user.
    /// </summary>
    internal MapContainer<int, Sequence<string>> waitingForDelivery =
      new MapContainer<int,Sequence<string>>();
  }             
    /// <summary>
    /// A mapping from logged-on users to their associated data.
    /// </summary>
    static MapContainer<int, User> users = new MapContainer<int,User>();
      // ...   
  }

Como pode ser visto, definir um estado de modelo não é muito diferente de definir uma classe C# normal. Os métodos de regra são métodos C# para descrever em que estado uma ação pode ser ativada. Quando isso acontece, também descreve que tipo de atualização é aplicado ao estado do modelo. Aqui, uma “LogonRequest” serve como exemplo para ilustrar como escrever um método de regra:

[Rule]
static void LogonRequest(int userId)
{
  Condition.IsTrue(!users.ContainsKey(userId));
  User user = new User();
  user.state = UserState.WaitingForLogon;
  user.waitingForDelivery = new MapContainer<int, Sequence<string>>();
  users[userId] = user;
}

Esse método descreve a condição de ativação e a regra de atualização da ação “LogonRequest”, que foi declarada anteriormente no código Cord. Essa regra essencialmente diz:

  • A ação LogonRequest pode ser executada quando o userId da entrada ainda não existir no conjunto de usuários atual. “Condition.Is­True” é uma API fornecida pelo Spec Explorer para especificar uma condição de habilitação.
  • Quando essa condição for atendida, um novo objeto de usuário será criado com seu estado apropriadamente inicializado. Ele é, então, adicionado à coleção de usuários globais. Essa é a parte da “atualização” da regra.

Nesse ponto, a maior parte do trabalho de modelagem está concluído. Vamos agora definir algumas “máquinas” para que possamos explorar o comportamento do sistema e obter alguma visualização. No Spec Explorer, máquinas são unidades de exploração. Uma máquina tem um nome e um comportamento associado definido na linguagem Cord. Também é possível compor uma máquina com outras para formar um comportamento mais complexo. Vamos ver algumas máquinas de exemplo para o modelo do chat:

machine ModelProgram() : Actions
{
  construct model program from Actions where scope = "Chat.Model"
}

A primeira máquina que definimos é uma máquina chamada “programa de modelo”. Ela usa a diretiva “construct model program” para dizer ao Spec Explorer que explore o comportamento inteiro do modelo baseado em métodos de regra encontrados no namespace Chat.Model:

machine BroadcastOrderedScenario() : Actions
{
  (LogonRequest({1..2}); LogonResponse){2};
  BroadcastRequest(1, "1a");
  BroadcastRequest(1, "1b");
  (BroadcastAck)*
}

A segunda máquina é um “cenário,” um padrão de ações definidas de uma forma como expressão regular. Cenários são normalmente compostos com uma máquina “programa de modelo” para dividir o comportamento total, como a seguir:

machine BroadcastOrderedSlice() : Actions
{
  BroadcastOrderedScenario || ModelProgram
}

O operador “||” cria uma “composição paralela sincronizada” entre as duas máquinas participantes. O comportamento resultante conterá apenas as etapas que podem ser sincronizadas em ambas as máquinas (com “sincronizadas” queremos dizer ter a mesma ação com a mesma lista de argumentos). Explorar essa máquina resulta no gráfico mostrado na Figura 3.

Composing Two Machines
Figura 3 Compondo duas máquinas

Como pode ser visto do gráfico na Figura 3, o comportamento composto é compatível com a máquina de cenário e o programa de modelo. Essa é uma técnica avançada para obter um subconjunto mais simples de um comportamento complexo. Ainda, quando seu sistema tem espaço de estado infinito (como no caso do sistema de chat), dividir o comportamento total pode gerar um subconjunto finito mais adequado para fins de teste.

Vamos analisar as diferentes entidades nesse gráfico. Os estados em círculos são controláveis. São estados em que estímulos são fornecidos para o SUT. Os estados em losangos são observáveis. São estados em que um ou mais eventos são esperados do SUT. O oracle de teste (o resultado esperado do teste) já está codificado no gráfico com etapas de eventos e seus argumentos. Os estados com diversas etapas de evento de saída são chamados estados não determinísticos, pois o evento fornecido pelo SUT em tempo de execução não é determinado em tempo de modelagem. Observe que o gráfico de exploração na Figura 3 contém diversos estados não determinísticos: S19, S20, S22 e assim por diante.

O gráfico explorado é útil para entender o sistema, mas ainda não é adequado para teste, pois não está na forma de “teste normal”. Dizemos que um comportamento está na forma normal de teste se ele não contém nenhum estado que tenha mais de uma etapa de retorno de chamada de saída. No gráfico da Figura 3, é possível ver que S0 viola essa regra de forma óbvia. Para converter tal comportamento em forma de teste normal, é possível simplesmente criar uma nova máquina usando o constructo dos casos de teste:

machine TestSuite() : Actions where TestEnabled = true
{
  construct test cases where AllowUndeterminedCoverage = true
  for BroadcastOrderedSlice
}

Esse constructo gera um novo comportamento percorrendo o comportamento original e gerando rastreamentos na forma de teste normal. O critério de passagem é cobertura de borda. Cada etapa do comportamento original é coberta pelo menos uma vez. O gráfico da Figura 4 mostra o comportamento depois de tal passagem.

Generating New Behavior
Figura 4 Gerando novo comportamento

Para alcançar a forma de teste normal, os estados com diversas etapas de retorno de chamada são divididos em um por etapa. As etapas de eventos não são nunca divididas e são sempre totalmente preservadas, pois os eventos são as opções que o SUT pode fazer em tempo de execução. Os casos de teste devem estar preparados para lidar com qualquer opção possível.

O Spec Explorer pode gerar código do conjunto de testes de um comportamento de forma de teste normal. A forma padrão do código de teste gerado é um teste de unidade do Visual Studio. É possível executar diretamente tal conjunto de testes com as ferramentas de teste do Visual Studio, ou com a ferramenta de linha de comando mstest.exe. O código do conjunto de testes gerado é legível e pode ser facilmente depurado:

#region Test Starting in S0
[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()]
public void TestSuiteS0() {
  this.Manager.BeginTest("TestSuiteS0");
  this.Manager.Comment("reaching state \'S0\'");
  this.Manager.Comment("executing step \'call LogonRequest(2)\'");
  Chat.Adapter.ChatAdapter.LogonRequest(2);
  this.Manager.Comment("reaching state \'S1\'");
  this.Manager.Comment("checking step \'return LogonRequest\'");
  this.Manager.Comment("reaching state \'S4\'");
  // ...
    }

O gerador de código de teste é altamente personalizável e pode ser configurado para gerar casos de teste que visam diferentes estruturas de teste, como a NUnit.

O modelo de Chat completo está incluído no instalador do Spec Explorer.

Quando o MBT compensa?

Há prós e contras ao usar o teste baseado em modelo. A vantagem mais óbvia é que depois da conclusão do modelo de teste, é possível gerar casos de teste pressionando um botão. Além disso, o fato de que um modelo tem de ser formalizado de início permite detecção antecipada de inconsistências nos requisitos e ajuda as equipes a estarem de acordo em termos de comportamento esperado. Observe que ao escrever casos de teste manuais, o “modelo” também está lá, mas não está formalizado e reside na cabeça do testador. O MBT força a equipe de teste a comunicar claramente suas expectativas em termos de comportamento do sistema e anotá-las usando uma estrutura bem-definida.

Outra clara vantagem é que a manutenção do projeto é menor. Mudanças no comportamento do sistema ou recursos recém-adicionados podem ser refletidos com a atualização do modelo, que normalmente é muito mais simples do que alterar casos de teste manualmente, um a um. Identificar apenas os casos de teste que precisam ser alterados é algumas vezes uma tarefa demorada. Considere também que a criação de modelo é independente da implementação ou do teste real. Isso significa que diferentes membros de uma equipe podem trabalhar em diferentes tarefas simultaneamente.

Como desvantagem, um ajuste de mentalidade é necessário com frequência. Esse é talvez um dos principais desafios dessa técnica. Além do problema bem-conhecido de que as pessoas do setor de TI não têm tempo de testar novas ferramentas, a curva de aprendizado para usar essa técnica não é negligenciável. Dependendo da equipe, aplicar MBT pode exigir algumas mudanças de processo também, que podem gerar, da mesma forma, alguma resistência.

A outra desvantagem é que você tem de fazer mais trabalho antecipadamente, assim, leva mais tempo para ver o primeiro caso de teste sendo gerado, comparando com a utilização de casos de teste tradicionais, escritos manualmente. Além disso, a complexidade do projeto de teste precisa ser suficientemente grande para justificar o investimento.

Felizmente, há algumas regras básicas que acreditamos ajudar a identificar quando o MBT realmente compensa. Ter um conjunto infinito de estados de sistema com requisitos que podem ser cobertos de diferentes formas é um primeiro sinal. Um sistema reativo ou distribuído, ou um sistema com interações assíncronas ou não determinísticas é outro. Ainda, métodos que têm muitos parâmetros complexos podem apontar na direção do MBT.

Quando essas condições são atendidas, o MBT pode fazer uma grande diferença e economizar esforço de teste significativo. Um exemplo disso é o Microsoft Blueline, um projeto em que centenas de protocolos foram verificados como parte da iniciativa de conformidade com o protocolo Windows. Nesse projeto, usamos o Spec Explorer para verificar a precisão técnica da documentação do protocolo com respeito ao comportamento real do protocolo. Esse foi um esforço gigantesco e a Microsoft gastou cerca de 250 pessoas-ano no teste. O Microsoft Research validou um estudo estatístico que mostrava que com a utilização do MBT a Microsoft economizava 50 pessoas-ano de trabalho de testador, ou cerca de 40% do esforço comparado com uma abordagem de teste tradicional.  

O teste baseado em modelo é uma poderosa técnica que adiciona uma metodologia sistemática às técnicas tradicionais. O Spec Explorer é uma ferramenta madura que utiliza os conceitos de MBT em um avançado ambiente de desenvolvimento altamente integrado como uma Power Tool do Visual Studio gratuita.

Yiming Cao é líder de desenvolvimento sênior da equipe Microsoft Interop and Tools e trabalha no Protocol Engineering Framework (incluindo Microsoft Message Analyzer) e no Spec Explorer. Antes de ingressar na Microsoft trabalhou na IBM Corp. em seu pacote de colaboração empresarial e depois ingressou em uma empresa startup trabalhando em tecnologias de streaming de mídia.

Sergio Mera é gerente de programa sênior da equipe Microsoft Interop and Tools e trabalha no Protocol Engineering Framework (incluindo Microsoft Message Analyzer) e no Spec Explorer. Antes de ingressar na Microsoft foi pesquisador e professor acadêmico do Departamento de Ciência da Computação da Universidade de Buenos Aires e trabalhou em lógica modal e demonstração de teoremas automatizada.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Nico Kicillof (Microsoft)
Nico Kicillof (nicok@microsoft.com) é gerente-chefe de programa de Ferramentas de desenvolvimento e arquitetura de compilação do Windows Phone. Seu trabalho consiste em criar ferramentas e métodos que permitem aos engenheiros compilar, testar e manter produtos de software. Antes de ingressar na Microsoft, ele foi professor e vice-presidente do Departamento de Ciência da Computação da Universidade de Buenos Aires.