Preparei para este post a demonstração de uma biblioteca que descobri durante o curso Advanced Unit Testing da PluralSight.
Ela é responsável por facilitar a comparação entre objetos, mesmo que estes sejam de diferentes tipos. Mostrarei como seu uso pode ser útil para simplificarmos os asserts de nossos testes de unidade.
COMPARAÇÃO SEMÂNTICA
Existem objetos dentro de um software que, mesmo sendo de tipos diferentes, são semanticamente similares.
Um exemplo disso são aqueles “objetos” que usamos para transitar dados entre as camadas do software. Na camada de apresentação, podemos ter uma viewmodel e, na camada de aplicação, podemos ter um outro tipo de objeto que é criado a partir dos dados da viewmodel.
Em algumas situações, como nos testes de unidade, precisamos saber se estes objetos são semelhantes, isto é, se possuem os mesmos dados.
Para melhor compreensão, vejamos o código abaixo, onde testamos o mapeamento de um objeto da camada de apresentação (CriacaoDeClienteViewModel) para um objeto da camada de aplicação (CriacaoDeClienteCmd):
[TestFixture] public class MapeadorTeste { [Test] public void Deve_mapear_viewmodel_para_cmd() { // arrange var vm = new CriacaoDeClienteViewModel { Nome = "Robson", Email = "rcs@rcs.com", NumeroDaMatricula = "123456" }; var mapeador = new Mapeador(); // act CriacaoDeClienteCmd cmd = mapeador.Mapear(vm); // assert Assert.AreEqual(cmd.Nome, vm.Nome); Assert.AreEqual(cmd.Email, vm.Email); Assert.AreEqual(cmd.NumeroDaMatricula, vm.NumeroDaMatricula); } }
Percebam pelas 3 asserções, que o que desejamos testar é que o objeto destino foi preenchido corretamente com os dados que vieram do objeto origem. Isso acaba gerando uma pequena poluição nos testes (dadas as 3 asserções). E poderiam haver mais asserções se os objetos tivessem mais dados!
RESOLVENDO COM EQUALS
Uma ideia para deixar o teste mais limpo seria implementar Equals() no objeto destino e usar um objeto esperado no teste:
[TestFixture] public class MapeadorTeste { [Test] public void Deve_mapear_viewmodel_para_cmd() { // arrange var vm = new CriacaoDeClienteViewModel { Nome = "Robson", Email = "rcs@rcs.com", NumeroDaMatricula = "123456" }; var mapeador = new Mapeador(); // act CriacaoDeClienteCmd cmd = mapeador.Mapear(vm); // assert var cmdEsperado = new CriacaoDeClienteCmd { Nome = vm.Nome, Email = vm.Email, NumeroDaMatricula = vm.NumeroDaMatricula }; Assert.AreEqual(cmdEsperado, cmd); } }
Para o teste acima passar, precisaríamos implementar Equals() em CriacaoDeClienteCmd (*).
Apesar de uma boa ideia para limpar o teste, pode ser trabalhoso implementar Equals() para vários desses objetos unicamente para fins de teste (e também custoso de mantê-los caso seja frequente a inclusão/exclusão de propriedades desses objetos).
(*) Há outras soluções para evitar a implementação de Equals() no objeto destino só por causa dos testes, como criar um helper que implementa a igualdade no próprio projeto de teste ou ainda criar uma classe que herda de CriacaoDeClienteCmd – também no projeto de teste – e implementar o Equals() nela. No entanto, todas essas soluções caem nos problemas citados no parágrafo acima
RESOLVENDO COM A LIB SEMANTICCOMPARISON
Uma alternativa para resolver o problema de poluição nas asserções dos testes, é utilizar uma biblioteca especifica para comparação semântica – SemanticComparison – disponível pelo Nuget e criada pelo instrutor do curso: Mark Seemann.
Vejamos abaixo como fica o teste anterior usando a lib:
[TestFixture] public class MapeadorTeste { [Test] public void Deve_mapear_viewmodel_para_cmd() { // arrange var vm = new CriacaoDeClienteViewModel { Nome = "Robson", Email = "rcs@rcs.com", NumeroDaMatricula = "123456" }; var mapeador = new Mapeador(); // act CriacaoDeClienteCmd cmd = mapeador.Mapear(vm); // assert var cmdEsperado = vm.AsSource().OfLikeness<CriacaoDeClienteCmd>(); cmdEsperado.ShouldEqual(cmd); } }
A diferença em relação ao teste anterior está em apenas duas linhas:
– Ao invés de fazermos “new CriacaoDeClienteCmd()”, criamos de forma fluente um objeto do tipo Likeness, fazendo: vm.AsSource().OfLikeness<CriacaoDeClienteCmd>(). Notem que começamos pela origem (a viewmodel “vm”) e dizemos que ela é semelhante ao destino (OfLikeness<CriacaoDeClienteCmd>).
– Fazemos a asserção através do objeto Likeness<CriacaoDeClienteViewModel, CriacaoDeClienteCmd> retornado anteriormente.
A vantagem é que não precisamos implementar Equals() em CriacaoDeClienteCmd e não precisaremos mais alterar a asserção e o valor esperado mesmo que as propriedades dos objetos origem e destino sejam alteradas.
OUTRO CENÁRIO
É possível usar a lib SemanticComparison também em testes de comportamento (ou interação), ou seja, quando fazemos uso de um mock para verificar se determinado comando foi invocado. (Obs.: para criação do mock, estou usando a biblioteca Moq.)
Observem o exemplo abaixo:
[TestFixture] public class ServicoTeste { [Test] public void Deve_notificar_sobre_o_evento() { // arrange var notificadorMock = new Mock<INotificador>(); var servico = new Servico(notificadorMock.Object); var evento = new ClienteFoiCadastrado { IdDoCliente = 1000 }; // act servico.Executar(); // assert var esperado = evento.AsSource().OfLikeness<ClienteFoiCadastrado>() .Without(c => c.Id) .CreateProxy(); notificadorMock.Verify(n => n.NotificarSobre(esperado)); } } // classe testada public class Servico { // construtor e demais membros public void Executar() { // qualquer outro codigo... _notificador.NotificarSobre(new ClienteFoiCadastrado { IdDoCliente = 1000 }); } }
Notem na fase de arrange do teste que agora usamos como fonte o próprio objeto esperado (ClienteFoiCadastrado) e, ao invés de criamos um objeto Likeness para poder fazer a asserção, retornamos um proxy do objeto esperado chamando o método CreateProxy() ao final da cadeia fluente.
Utilizamos o proxy criado como parâmetro do método NotificarSobre() na fase de asserção, sendo assim, o teste irá passar se o método tiver sido chamado com o parâmetro semelhante (mesmos dados) na classe sob teste.
Antes de concluirmos, você deve ter notado a chamada ao método Without(c => c.Id) na criação do proxy. Como pode-se deduzir pelo seu nome, este método permite que deixemos de fora da comparação algumas propriedades que não fazem sentido. Para isso ficar claro no exemplo, precisamos ver o restante do código, omitido anteriormente:
public abstract class Evento { public Guid Id { get; private set; } protected Evento() { Id = Guid.NewGuid(); } } public class ClienteFoiCadastrado : Evento { public int IdDoCliente { get; set; } }
Notem que o campo “Id”, usado no método Without(), é um GUID. Sendo assim, o objeto criado no teste e o objeto real (criado dentro do método Executar()) nunca possuirão o mesmo valor para “Id”. O que importa, neste caso, é garantirmos que os dados de ambos são os mesmos, ou seja, que eles são semelhantes. Por isso, desprezamos esse Id gerado automaticamente.
Bacana, não?
CONCLUSÃO
Neste post, vimos o conceito de comparação semântica e como usar a biblioteca SemanticComparison para fazermos comparação desse tipo em nossos testes de unidade com o propósito de deixarmos mais simples a fase de asserção dos testes.
É uma boa alternativa quando possuirmos muitos testes com várias linhas de asserção e for muito custoso implementar as comparações por nós mesmos.
Vale ressaltar que o foco do post foi na fase de asserção dos testes. Existem técnicas para deixar a fase de montagem do cenário (arrange) mais simples e menos frágil também, como o uso de builders.
Obs.:
1. O código-fonte completo deste post está no meu <github>.
2. Se você quer entender melhor o que é um mock, dê uma olhada <neste meu post>, onde mostro como implementar um SEM o uso de um biblioteca de mocking.
Muito Bacana Castilho. Me poupou várias linhas de código. Inclusive já utilizo outra biblioteca do Seemann chamada AutoFixture, também é muito boa.
CurtirCurtir
Olá, Mariano.
Não tive a oportunidade de usar o AutoFixture. Num primeiro momento, achei meio poluído. Preciso dar uma brincada com ele.
(o fonte do SemanticComparison está junto com ele no github: https://github.com/AutoFixture/AutoFixture/tree/master/Src)
Obrigado por comentar.
[]s
CurtirCurtir
Você utiliza qual framework de testes Castilho? Utilizo o MSTest, porém o Seeman utiliza o xUnit no curso e achei bem interessante.
CurtirCurtir
Uso o NUnit.
Tbem achei bacana mas nunca cheguei a usar o xUnit.net.
[]
CurtirCurtir
Muito bom, não conhecia esse semantic comparison!
CurtirCurtir