Testes Unitários com C# e xUnit – Parte 3: Como testar métodos que tenham dependências usando Moq

Olá, pessoal. Tudo bem?

Sigo com a escrita de artigos sobre Testes Unitários aqui. Na parte 1 falei sobre O que são, para que servem e quais os obstáculos. Na parte 2 falei sobre como escrever seus primeiros testes e padrões para eles.

Neste artigo, seguirei com a abordagem mais prática e falarei sobre a importância de aplicar o princípio da inversão da dependência no seu código, o que são Mocks e como utilizar o pacote Moq para testar dependências externas do método a ser testado. O código utilizado nesse artigo está hospedado neste repositório.


Princípio da Inversão de Dependência

Um conjunto de princípios muito importante para os desenvolvedores conhecerem é o SOLID. Ele foi introduzido pelo mito Robert C Martin (Uncle Bob), e se referem a orientações para deixar o seu código mais flexível, compreensível e de melhor manutenção, de maneira geral.

Especificamente falo sobre o Princípio da Inversão de Dependência (com sigla em inglês DIP), já que vejo que é bastante importante nesse tema de testes unitários que venho cobrindo aqui.

Ele diz que módulos de alto nível não devem depender de módulos de baixo nível. Na prática, classes não devem depender de implementações, e sim de interfaces. Quando a implementação for alterada, considerando que as classes dependam de interfaces dessas classes concretas, alterações não seriam necessárias.

Por exemplo:

Classe ContaCorrenteService dependendo de uma implementação

Imagina que o ContaCorrenteService, após realizar alguma operação, lance uma notificação para usuários. Nesse caso, ele depende da classe concreta EmailService.

Se no futuro eu implementar uma nova classe para notificações, por exemplo, uma classe SmsService, eu precisaria alterar essa classe, e adicionar uma segunda dependência. A classe ContaCorrenteService precisaria ser alterada por essa adição de meio de notificação, e teria que explicitamente chamar o novo método.

Esse é um exemplo de classe que não segue o Princípio de Inversão de Dependência. O ideal seria que ela dependesse de uma interface como INotificacaoService, ficando desacoplada da implementação dela.

Exemplo melhorado:

Classe ContaCorrenteService dependendo de uma interface

O que são Mocks e para que servem

Importante conceito em testes unitários, mocks são objetos que simulam o comportamento de objetos reais. Podem definir retornos específicos em seus métodos, de maneira que a classe a consumir eles nem note que algo foi alterado.

Bom, tá. Para que servem?

Na nossa classe de ContaCorrenteService, vemos a dependência com INotificacaoService. Tudo ok, certo?

Ao escrever testes unitários para um método, por exemplo, que notifique uma conta corrente específica, queremos nos assegurar que o fluxo nesse método esteja correto.

Pode fazer sentido testar que a notificação esteja sendo gerada, mas isso pode causar um grande problema: por ser uma dependência externa, tempo maior de execução será adicionado na execução dos testes (resposta dos serviços externos de notificação). Além disso, testes unitários não servem a esse fim, senão testes de integração. Testes unitários servem para verificar o comportamento de uma unidade de código, como um método. Adicionar diferentes dependências concretas irá dificultar a análise do que está com comportamento errado no método testado, e aumentará o tempo de execução do suíte de testes.

Suíte de testes unitários tem que ser rápidos ao serem executados. Essa necessidade é intensificada ainda mais quando pensamos em esteiras de compilação e publicação, já que ao inserir a execução dos testes unitários nelas (o que é correto e esperado em ter) isso pode influenciar na agilidade da esteira toda.

Assim, Mocks devem ser utilizados quando possível nos testes unitários, para controlar o retorno dessas dependências externas, deixando o desenvolvedor concentrado no teste a ser escrito.


O pacote Moq e como utilizar

O pacote Moq é utilizado para criar Mocks. Vou mostrar o seu uso mais à frente, na prática.

Instalando no Visual Studio

Para instalar usando o Visual Studio, basta gerenciar os pacotes Nuget do projeto de testes unitários, e buscar por Moq.

Instalando o pacote Moq via Nuget Package Manager

Instalando no Visual Studio Code

Usando a dotnet CLI, via linha de comando, pode-se instalar Moq usando o comando abaixo:

Não foi fornecido texto alternativo para esta imagem
Instalando o pacote Moq via linha de comando

Terminado de instalar, vamos à escrita de testes unitários!

Apresentando a classe cujo método será testado

A classe a ser testada é a ContaCorrenteService. Para esse exemplo, ela contém apenas um método: NotificarContaCorrente, recebendo uma string representando o documento do dono da conta.

Ela tem como dependência as interfaces IContaCorrenteRepository e INotificacaoService. Como falado anteriormente, a dependência em interfaces mantém a aderência com o Princípio de Inversão de Dependência, melhorando a testabilidade da classe e seus métodos.

Escrevendo o teste unitário e utilizando Moq

Começamos com a criação do arquivo no projeto de testes unitários. Recomendo criar pastas para cada projeto a ser testado, ainda mais se você utilizar um projeto de testes unitários para tudo.

Criei uma pasta para Application, que é o projeto que contém a classe e método a serem testados, além das pastas internas Factories e Services. Em Factories, criei um método estático para retornar objetos controlados para nossos testes unitários (que servirão como mocks para as dependências da nossa classe) e em Services criei o arquivo e classe ContaCorrenteServiceNotificarContaCorrenteTests.

No nome da classe prefiro incluir o nome do método a ser testado. Com isso, os nomes dos métodos de teste conseguem ser escritos no padrão Given_When_Should sem preocupações adicionais com o tamanho. Além de que consigo deixar a classe de testes mais focada em uma funcionalidade, já que facilmente ela fica gigante se usar para diversos métodos e cenários.

Os dois cenários, inicialmente, são:

  • Se o serviço de notificação retorna um objeto de sucesso, o método NotificarContaCorrente deveria retornar true.
  • Se o serviço de notificação retorna um objeto de falha, o método NotificarContaCorrente deveria retornar false.

Cenário 1

Utilizando o padrão Given_When_Should, explicado no artigo anterior, cheguei no seguinte nome:

  • Given: ContaExistenteNotificacaoFuncionando
  • When: ChamadoComDocumentoValido
  • Should: RetornarSucesso

Assim, o nome completo fica ContaExistenteNotificacaoFuncionando_ChamadoDocumentoValido_RetornarSucesso.

Segue a implementação do teste unitário para esse cenário:

Primeiro teste unitário para NotificarContaCorrente

Para a implementação, utilizei o padrão Arrange, Act e Assert (vulgo AAA), explicado no artigo anterior. Vamos por partes:

  • Arrange: Nessa parte, obtemos os objetos controlados dos Factories. Em seguida, criamos os mocks das dependências utilizando a biblioteca Moq e configuramos os métodos para que retornem os objetos controlados. Podemos ler a primeira linha de Setup como “o mock contaCorrenteRepositoryMock da dependência, quando o seu método ObterDocumento for chamado com parâmetro documento do objeto ContaCorrente, deve retornar contaCorrente”. Finalmente, instanciamos a classe cujo método será testado, passando como suas dependências os mocks configurados.
  • Act: A parte mais simples. Simplesmente chame o método a ser testado, e armazene seu valor (ou não, se for um método sem retorno).
  • Assert: Utilizando a biblioteca Moq conseguimos verificar se os métodos de suas dependências foram chamados, utilizando o método Verify desde os mocks, e passando como segundo parâmetro um struct do tipo Time da biblioteca. Além disso, verificamos se a resposta do método a ser testado é a mesma do status de sucesso da notificação (que está retornando true, pelo objeto controlado do Factory).

Cenário 2

Utilizando o padrão Given_When_Should, explicado no artigo anterior, cheguei no seguinte nome:

  • Given: ContaExistenteMasNotificacaoNaoFuncionando
  • When: ChamadoComDocumentoValido
  • Should: RetornarFalha

Assim, o nome completo fica ContaExistenteMasNotificacaoNaoFuncionando_ChamadoDocumentoValido_RetornarFalha.

Segue a implementação do teste unitário para esse cenário:

Teste para falha em método NotificarContaCorrente

Os processos de Assert, Act, Assert são bem semelhantes ao do cenário 1. A única diferença é que um outro método de Factory da resposta é utilizado (retornando uma resposta de falha) e no Assert agora é verificada que a resposta da notificação é false.

Atividade proposta

Que tal criar um fork do projeto no GitHub e implementar teste unitário que cubra se o documento ou a resposta da dependência sobre Repository retornar null? Aproveite para compartilhar os resultados na sessão de comentários!


Livros sobre Testes Unitários

Assim como comentei neste artigo, onde indiquei 3 livros essenciais para desenvolvedores .NET, os livros seguem sendo uma ótima fonte de informação. Sendo assim, decidi deixar indicação de livros sobre o tema.

Test Driven Development: By Example, Kent Beck

Livro escrito pela lenda Kent Beck, criador do Extreme Programming e Test-Driven Development, e um dos signatários originais do Manifesto Ágil. Livro fantástico sobre o tema, para quem quiser se aprofundar no assunto.

O livro pode ser encontrado aqui, em formato físico.

Test-Driven Development: Teste e Design no Mundo Real com .NET, Casa do Código

Test-Driven Development: Teste e Design no Mundo Real com .NET por [Mauricio Aniche]

O livro pode ser encontrado aqui, em formato físico ou digital.

The Art of Unit Testing: With Examples in C#, Roy Osherove

Com prólogos de autores reconhecidos mundialmente como Michael Feathers e Robert C. Martin, este livro também é uma leitura altamente indicada para aqueles que querem se aprofundar em testes unitários.

O livro pode ser encontrado aqui, em formato físico.


Inscreva-se na lista de espera do Método .NET Direto ao Ponto, um treinamento completo sobre C#, APIs com ASP.NET Core e Microsserviços:  Inscreva-se aqui.

São quase 200 vídeo-aulas sobre temas como C#, ASP NET Core 5, EF Core, CQRS, Clean Architecture, Autenticação e autorização com JWT, Testes Unitários, além de mini-cursos em Microsserviços, Performance em .NET, ASP NET Core e Azure, Docker, Carreira Internacional em .NET, e mais.


Conclusão

Ufa, chegamos na terceira e última parte dessa série sobre testes unitários! Espero, sinceramente, que tenha agregado algo para vocês. Quaisquer dúvidas, só comentar ou mandar uma mensagem diretamente para mim e tentarei ajudar do jeito que for possível.

Estou pensando em fazer um live coding focado em testes unitários e boas práticas, cobrindo outros cenários em um projeto mais “real”. Trocaríamos ideias e eu tentaria ajudar com quaisquer dúvidas de vocês. O que acham? Comentem ou mandem mensagens com sua opinião disso, por favor!

Até a próxima!

Se achou o artigo interessante, te convido a comentar, e/ou compartilhar!