Tuesday, January 08, 2008 11:00 PM

Tanto no blog, quanto na coluna Tools, já falei bastante sobre ferramentas de Mocking. Para quem tem um pouco mais de experiência com testes unitários elas são bastante comuns. Porém o conceito e utilização delas não é muito intuitivo e pode ser confuso para os marinheiros de primeira viagem.

Por isto, inspirado neste ótimo artigo, resolvi tentar apresentar os conceitos e as práticas de Mocking de forma mais intuitiva. Não vou me preocupar muito em definir cada conceito, o que é um Stub, ou qual a sua diferença para um Mock. A idéia é seguir a linha de raciocínio que começou com soluções simples e culminou na criação destas ferramentas.

UPDATE: Link correto para o artigo: http://msdn.microsoft.com/msdnmag/issues/07/09/MockTesting/default.aspx?loc=en

Vamos começar com um simples teste unitário. O primeiro exemplo do QuickStart do NUnit ficou famoso, aquele velho exemplo da conta corrente. O código da conta corrente:

public class Conta
{
  private int id;
  private decimal saldo;

  public Conta(int id, decimal saldo)
  {
    this.id = id;
    this.saldo = saldo;
  }

  public decimal Saldo
  {
    get { return saldo; }
  }

  public void Sacar(decimal montante)
  {
    if (montante > saldo)
      throw new ApplicationException("Saldo insuficiente");

    saldo -= montante;
  }

  public void Depositar(decimal montante)
  {
    saldo += montante;
  }
}

Este código é facilmente testado com testes unitários como este:

[TestFixture]
public class ContaTest
{
  [Test]
  public void Sacar_ComSaldo()
  {
    decimal saldoAnterior = 1000;
    decimal montante = 500;
    decimal saldoFinal = saldoAnterior - montante;

    Conta conta = new Conta(1, saldoAnterior);

    conta.Sacar(montante);

    Assert.AreEqual(saldoFinal, conta.Saldo, "Saldo não bate");
  }
}

Pela sua característica, este teste é classificado como teste de estado, pois ele valida o estado do objeto após a execução de um método.

A coisa complica quando o código fica mais complexo e realista. O código abaixo é o método Transferir, implementado na classe ServicoConta. Este método recebe id’s de duas contas e faz a transferência do valor de uma conta para outra. Com o id da conta, a classe pede para outra classe, RepositorioConta, que busque os dados da conta na base de dados.

public class ServicoConta
{
  public void Transferir(int idContaDebito, int idContaCredito, decimal montante)
  {
    Conta contaDebito = BuscarConta(idContaDebito);
    Conta contaCredito = BuscarConta(idContaCredito);

    contaDebito.Sacar(montante);
    contaCredito.Depositar(montante);
  }

  private Conta BuscarConta(int idConta)
  {
    IRepositorio<Conta> repositorio = new RepositorioConta();
    return repositorio.Buscar(idConta);
  }
}

Podíamos discutir se a responsabilidade de instanciar as conta é realmente desta classe, mas isto não importa muito, porque no final das contas esta responsabilidade será de alguém, e com isto vem a dependência da classe RepositorioConta. Isto estraga meu teste unitário, agora ele depende do bom funcionamento da classe RepositorioConta e também dos dados residentes no banco de dados.

O ideal seria conseguir emular a classe RepositorioConta para conseguirmos um comportamento consistente, sem precisar “confiar” em outros comportamentos. Este é o objetivo dos Stubs, Mocks e/ou Test doubles. Esta implementação da interface IRepositorio<Conta> é justamente isto. Nela vou poder inserir contas e saber exatamente qual conta o método Buscar irá retornar. Com isto, tenho o controle do comportamento do repositório.

public class RepositorioContaFake : IRepositorio<Conta>
{
  private Dictionary<int, Conta> contas = new Dictionary<int, Conta>();

  public Dictionary<int, Conta> Contas
  {
    get { return contas; }
    set { contas = value; }
  }

  public Conta Buscar(int id)
  {
    if (!contas.ContainsKey(id))
      throw new ApplicationException(string.Format("não existe conta com id = {0}", id));

    return contas[id];
  }
}

Agora é preciso alterar o ServicoConta para conseguir substituir a implementação verdadeira do IRepositorio pela RepositorioContaFake. Para não complicar o exemplo vou simplesmente criar uma propriedade que me permite injetar uma implementação de IRepositorio para substituir a verdadeira.

public class ServicoConta
{
  private IRepositorio<Conta> repositorio;

  public IRepositorio<Conta> Repositorio
  {
    private get
    {
      if (repositorio == null)
        repositorio = new RepositorioConta();

      return repositorio;
    }
    set { repositorio = value; }
  }

  public void Transferir(int idContaDebito, int idContaCredito, decimal montante)
  {
    Conta contaDebito = BuscarConta(idContaDebito);
    Conta contaCredito = BuscarConta(idContaCredito);

    contaDebito.Sacar(montante);
    contaCredito.Depositar(montante);
  }

  private Conta BuscarConta(int idConta)
  {
    return Repositorio.Buscar(idConta);
  }
}

O teste fica um pouco longo, com toda a preparação do RepositorioFake, mas alcanço meu objetivo. Este teste também é um teste de estado. Depois de realizar as operações necessárias, verifico o saldo das duas contas, ou seja, o estado de cada uma delas.

[TestFixture]
public class ServicoContaTest
{
  [Test]
  public void Transferir_ComSaldo()
  {
    decimal montante = 500;

    int idContaDebito = 1;
    Conta contaDebito = new Conta(idContaDebito, 1000);
    decimal saldoEsperadoContaDebito = contaDebito.Saldo - montante;

    int idContaCredito = 2;
    Conta contaCredito = new Conta(idContaCredito, 500);
    decimal saldoEsperadoContaCredito = contaCredito.Saldo + montante;

    RepositorioContaFake repositorioFake = new RepositorioContaFake();
    repositorioFake.Contas.Add(contaDebito.Id, contaDebito);
    repositorioFake.Contas.Add(contaCredito.Id, contaCredito);

    ServicoConta servico = new ServicoConta();
    servico.Repositorio = repositorioFake;

    servico.Transferir(idContaDebito, idContaCredito, montante);

    Assert.AreEqual(saldoEsperadoContaDebito, contaDebito.Saldo);
    Assert.AreEqual(saldoEsperadoContaCredito, contaCredito.Saldo);
  }
}

Na próxima parte vou apresentar outro exemplo e novas necessidades que vão surgir para conseguirmos fazermos nossos testes unitários.

< Exemplos >

Comments

At 1/9/2008 4:19 PM, Max said:

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

O link do artigo está quebrado. Parabéns pelo post.
At 1/9/2008 9:31 PM, Eduardo Miranda said:

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

Max, obrigado pelo aviso. Veja o link correto:
http://msdn.microsoft.com/msdnmag/issues/07/09/MockTesting/default.aspx?loc=en
Post Comment
Title *
Name *
Email (never displayed)
Website
Comment * (Allowed tags: blockquote, a, strong, em, p, u, strike, super, sub, code)  
Please add 4 and 7 and type the answer here: