Sunday, January 13, 2008 4:05 PM

Continuo a série sobre mocks. A primeira parte mostrou como podemos criar objetos falsos, cujo comportamento eu controlo facilmente, para executar testes unitários sem depender do comportamento dos objetos reais. Vamos aprofundar um pouco o assunto.

Vou utilizar outro exemplo bem manjado, que é o cadastro de cliente. Na minha implementação o ServicoCliente é responsável por esta tarefa. Além de persistir o cliente, utilizando o RepositorioCliente, o processo precisa antes validar o CPF do cliente, acessando um serviço externo. Depois de tudo ainda é necessário enviar um e-mail de boas-vindas para o cliente.

public class ServicoCliente
{
  private readonly IValidacao validador = new ValidacaoTabajara();
  private IRepositorio<Cliente> repositorio = new RepositorioCliente();
  private IMailer mailer = new Mailer();

  public void CadastrarCliente(Cliente cliente)
  {
    ValidarCpf(cliente);
    Salvar(cliente);
    EnviarEmailBoasVindas(cliente);
  }

  private void EnviarEmailBoasVindas(Cliente cliente)
  {
    string sender = "";
    string body = "";
    string subject ="";

    mailer.EnviarEmail(cliente.Email, subject, body, sender);
  }

  private void Salvar(Cliente cliente)
  {
    repositorio.Salvar(cliente);
  }

  private void ValidarCpf(Cliente cliente)
  {
    validador.ValidarCpf(cliente.Cpf);
  }
}

Isolar as dependências externas a gente já sabe como fazer, basta criar “fakes” para cada um dos objetos. Fazendo um teste semelhante aos que fiz no artigo anterior, eu injeto os objetos falsos no ServicoCliente e envio um objeto Cliente para o método CadastrarCliente. Mas o problema aqui é outro, um teste de estado, neste caso, tem pouca valia. O objeto Cliente não tem grandes alterações de estado. O que importa neste método é o fluxo de atividades: validar o CPF, salvar o cliente e depois enviar o e-mail de boas-vindas.

Como é possível testar o comportamento de um método? É preciso aumentar mais um pouco as funcionalidades de nossos objetos falsos. Veja como este objeto grava a chamada do método, inclusive os parâmetros utilizados, e oferece um método para validação desta chamada.

public class RepositorioClienteFake : IRepositorio<Cliente>
{
  private Cliente chamadaSalvar;

  public void Salvar(Cliente item)
  {
    GravarChamada(item);
  }

  private void GravarChamada(Cliente item)
  {
    chamadaSalvar = item;
  }

  public bool ValidarChamada(Cliente cliente)
  {
    return cliente == chamadaSalvar;
  }
}

Quando o método CadastrarCliente chamar o método Salvar, do repositório falso, este irá gravar a chamada. Ao final de tudo nosso teste chama o método ValidarChamada para verificar se o método realmente foi chamado. Preciso fazer isto para cada um dos objetos falsos, é bem trabalhoso. Veja como fica o teste no final das contas.

[Test]
public void CadastrarCliente_CpfOk()
{
  string cpf = "00000000000";
  string sender = "email@domainname.com";
  string body = "texto";
  string subject = "titulo";
  string emailCliente = "emailCliente@domainname.com";


  Cliente cliente = new Cliente(cpf);
  cliente.Email = emailCliente;

  //Cria o servico e injeta os objetos fakes
  ServicoCliente servico = new ServicoCliente();

  MailerFake mailer = new MailerFake();
  servico.Mailer = mailer;
  RepositorioClienteFake repositorio = new RepositorioClienteFake();
  servico.Repositorio = repositorio;
  ValidadorFake validador = new ValidadorFake();
  servico.Validador = validador;

  servico.CadastrarCliente(cliente);

  //Valida cada mock
  validador.ValidarChamada(cpf);
  repositorio.ValidarChamada(cliente);

  string[] parametros = { emailCliente, subject, body, sender };
  mailer.ValidarChamada(parametros);
}

Mesmo depois disto tudo ainda tenho um problema. Garanto que os métodos foram chamados, mas não a ordem em que isto aconteceu. Alguém pode mudar as ordem das chamadas e começar a enviar e-mails antes de validar o Cpf, o que seria errado.

Uma possível solução é criar eventos nos objetos fakes, que são executados sempre que os métodos são chamados e uma factory responsável por instanciar os objetos fakes. A factory passa a escutar os eventos e grava a ordem de chamada. Depois de tudo ela compara a ordem esperada com a ordem real.

Cada objeto fake passa a ter um evento, que é chamado quando o método é executado, como fiz com o MailerFake:

public class MailerFake : IMailer
{
  private string[] chamadaEnviarEmail;

  public void EnviarEmail(string email, string subject, string body, string sender)
  {
    string[] parametros = { email, subject, body, sender };
    GravarChamada(parametros);
  }

  private void GravarChamada(string[] parametros)
  {
    chamadaEnviarEmail = parametros;
    GravarCalled("Mailer.EnviarEmail", null);
  }

  public bool ValidarChamada(string[] parametros)
  {
    if (parametros.Equals(chamadaEnviarEmail))
      return false;

    return true;
  }

  public event EventHandler GravarCalled;
}

A factory tem um método que retorna cada fake que será utilizado, antes de retorná-lo, ela assina o evento, toda vez que um método é chamado, ela grava a chamada. O método GravarChamadaEsperada permite que o teste informe o que é esperado. Finalmente o método ValidarOrdemChamada, compara o esperado com o realizado.

public class FakeFactory
{
  private RepositorioClienteFake repositorio;
  private MailerFake mailerFake;
  private ValidadorFake validadorFake;

  private readonly Stack<string> actualCalls = new Stack<string>();
  private readonly Stack<string> expectedCalls = new Stack<string>();

  public RepositorioClienteFake GetRepositorioFake()
  {
    repositorio = new RepositorioClienteFake();
    repositorio.GravarCalled += GravarCalled;

    return repositorio;
  }

  public MailerFake GetMailerFake() ...
  public ValidadorFake GetValidadorFake() ...

  void GravarCalled(object sender, EventArgs e)
  {
    actualCalls.Push((string) sender);
  }

  public void GravarChamadaEsperada(string methodName)
  {
    expectedCalls.Push(methodName);
  }

  public void ValidarOrdemChamada()
  {
    if ( expectedCalls.Count != actualCalls.Count )
      throw new ApplicationException("O numero de chamadas executadas é diferente das esperadas");

    for(int i = 0; i < expectedCalls.Count; i++)
    {
      string expectedCall = expectedCalls.Pop();
      string actualCall = actualCalls.Pop();

      if (expectedCall != actualCall)
        throw new ApplicationException(string.Format("A chamada realizada: {0} é diferente da esperada: {1}",
                                                     actualCall, expectedCall));
    }
  }

O teste também muda, os objetos fakes passam a ser solicitados à factory, a ordem de chamada esperada dos métodos deve ser informada também à factory. Ao final ele solicita a validação desta ordem.

[Test]
public void CadastrarCliente_CpfOk()
{
  string cpf = "00000000000";
  string sender = "email@domainname.com";
  string body = "texto";
  string subject = "titulo";
  string emailCliente = "emailCliente@domainname.com";


  Cliente cliente = new Cliente(cpf);
  cliente.Email = emailCliente;

  //Cria uma fake factory e constroi os objetos
  FakeFactory factory = new FakeFactory();
  MailerFake mailer = factory.GetMailerFake();
  RepositorioClienteFake repositorio = factory.GetRepositorioFake();
  ValidadorFake validador = factory.GetValidadorFake();

  //Define ordem das chamadas
  factory.GravarChamadaEsperada("Validador.ValidarCpf");
  factory.GravarChamadaEsperada("RepositorioCliente.Salvar");
  factory.GravarChamadaEsperada("Mailer.EnviarEmail");
  
  //Cria o servico e injeta os objetos fakes
  ServicoCliente servico = new ServicoCliente();
  servico.Mailer = mailer;
  servico.Repositorio = repositorio;
  servico.Validador = validador;

  servico.CadastrarCliente(cliente);

  //Valida cada mock
  validador.ValidarChamada(cpf);
  repositorio.ValidarChamada(cliente);

  string[] parametros = { emailCliente, subject, body, sender };
  mailer.ValidarChamada(parametros);
      
  //Valida a ordem de chamada
  factory.ValidarOrdemChamada();
}

Finalmente está pronto o teste de comportamento. Poderia listar uma série de problemas que esta solução tem, a começar pela quantidade de código-fonte criado só para um teste. Existe uma ótima possibilidade de reaproveitarmos código criando uma ferramenta que possa ser utilizada em qualquer teste. É ai que entram as ferramentas que mocks. Mas vamos deixar isto para o próximo post.

< Exemplos >

Comments

At 1/15/2008 9:47 AM, Israel Aece said:

# re: Stubs, Test doubles e Mocks - Parte 2 de N

Boas Eduardo,

Tenho acompanhado de perto os teus posts sobre testes e gostaria de te perguntar algo:

Penso que a criação de "fakes", como você tem feito para testar partes da aplicação é um pouco trabalhosa e, gostaria de saber se, ferramentas como Rhino Mocks substituem essa necessidade.
At 1/15/2008 2:43 PM, Eduardo Miranda said:

# re: Stubs, Test doubles e Mocks - Parte 2 de N

Muito trabalhosa. O objetivo foi mostrar a origem do problema, as soluções que eram utilizadas no passado, tem gente que até hoje usa, para que os leitores entendam melhor os conceitos das ferramentas de mocks.

Como disse no final do post "É ai que entram as ferramentas que mocks".

A partir do próximo post vou começar apresentar as ferramentas e também as evoluções destas.

Aguardem novidades
Post Comment
Title *
Name *
Email (never displayed)
Website
Comment * (Allowed tags: blockquote, a, strong, em, p, u, strike, super, sub, code)  
Please add 8 and 1 and type the answer here: