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.