Comparando objetos semanticamente

Olá, galera

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.

Vamos então ao que interessa!

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();
        var cmdEsperado = new CriacaoDeClienteCmd { Nome = vm.Nome, Email = vm.Email, NumeroDaMatricula = vm.NumeroDaMatricula };

        // act
        CriacaoDeClienteCmd cmd = mapeador.Mapear(vm);

        // assert
        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();
        var cmdEsperado = vm.AsSource().OfLikeness<CriacaoDeClienteCmd>();

        // act
        CriacaoDeClienteCmd cmd = mapeador.Mapear(vm);

        // assert
        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 };
        var esperado = evento.AsSource().OfLikeness<ClienteFoiCadastrado>()
                                        .Without(c => c.Id).CreateProxy();
        // act
        servico.Executar();

        // assert
        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?

CONCLUINDO…

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.

Espero que tenham gostado. Comentem aí!

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.

Até a próxima!

Anúncios

4 comentários em “Comparando objetos semanticamente

Participe! Vamos trocar uma ideia sobre desenvolvimento de software!

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s