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.