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:
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:
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 no Visual Studio Code
Usando a dotnet CLI, via linha de comando, pode-se instalar Moq usando o comando abaixo:
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:
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:
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
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.
Quer alavancar sua carreira como Desenvolvedor(a) .NET?
Opa, aqui é o Luis Felipe (LuisDev), criador do blog LuisDev.
Além de Desenvolvedor .NET Sênior, eu sou instrutor de mais de 700 alunos e também tenho dezenas de mentorados.
Conheça o com mais de 800 video-aulas sobre C# e desenvolvimento de APIs com ASP NET Core, Microsserviços com ASP NET Core, Arquitetura de Software, Computação em Nuvem, SQL, HTML, CSS e JavaScript, JavaScript Intermediário, TypeScript, Desenvolvimento Front-End com Angular, e Desenvolvimento Front-end com React. Diversos mini-cursos disponíveis aos alunos e atualizações gratuitas.
Suporte dedicado, e comunidade de centenas de alunos.
Completo e online, destinado a profissionais que querem dar seu próximo passo em sua carreira como desenvolvedores .NET.
Clique aqui para ter mais informações e garantir sua vaga
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!