Testes Unitários com C# e xUnit – Parte 1: O que são, para que servem e quais os obstáculos

Olá, pessoal. Tudo bem?

Resolvi começar a escrever sobre testes unitários, já que vi que o material sobre isso não é muito fácil de encontrar. Além disso, é uma oportunidade de compartilhar minha experiência e mostrar algumas barreiras que encontrei nas equipes que participei quanto à sua adoção.


O que são testes unitários e quais seus benefícios?

De modo geral, Testes Unitários se referem à escrita de código para testar funções (ou unidades de funcionalidade) em seu código.

Vale a pena diferenciá-los dos testes de integração, que englobam dependências externas como acesso a banco de dados e outros serviços, por exemplo.

Vamos aos benefícios.

Cobertura de caminhos principais, alternativos e de exceção

Certo, mas por que eu gostaria de testar código que estou desenvolvendo? Teoricamente, ele já estaria fazendo o que deveria, certo? Ou você está insinuando que estou escrevendo código com falhas???

Calma. Escrever testes unitários para o seu código é necessário para que se encontrem falhas/casos em que você não pensou a princípio.

Por exemplo: considerando um e-commerce, será que sua regra de negócio permite que um pedido com zero itens seja criado? Acredito que não, certo?

Mas, considerando uma API, imagine que você não utiliza alguma biblioteca de validação para seu Controller (para detectar erros já no corpo da requisição de entrada). Esse objeto vai chegar na sua camada de Serviço/Aplicação, e dependendo de como seja sua utilização, poderá gerar inconsistências que podem afetar outras partes do sistema. E se de alguma maneira um pedido sem itens fosse criado, devido à falta de validação? Esse é só um exemplo, existem diversos outros em que a falta de cobertura de diversas situações do negócio pode trazer problemas.

Regressão de erros

Além disso, oferece um radar para regressão de erros, que seriam detectados assim que uma alteração incompatível fosse realizada no código coberto por testes.

Já passou pela situação em que um bug é corrigido, e após algumas semanas ele retorna magicamente e de maneira silenciosa? Isso pode ser causado por diversas más práticas, entre elas a de diversos programadores (orientados a diferentes stakeholders) realizarem alterações constantes no mesmo código. Conflitos de interesses pode ocorrer, e isso claramente mostra um problema de desenho do código. Através de testes unitários, preferivelmente executados em uma esteira de compilação de maneira automatizada, pode-se proteger seu código desses erros que seriam então detectados antes de alcançar o usuário através da execução deles.

Documentação de regras de negócio

Outra grande vantagem é que os testes unitários, quando bem escritos, fornecem praticamente uma documentação das regras do negócio do seu sistema. Quando testes unitários tem uma linguagem clara e são bem estruturados, é fácil de discutir a cobertura deles com analistas de negócio e outros stakeholders, além de auxiliar a integração de novos desenvolvedores. Estes poderão ler os testes unitários do módulo onde trabalharão, e com certeza terão uma visão melhor do produto em que trabalharão do que se tivessem que ir batendo o olho arquivo a arquivo do projeto.

Finalmente, uma vantagem que acredito ser de igual ou maior importância que as outras é que o desenho do seu código ficará melhor. Falarei mais sobre isso a seguir, citando as dificuldades em se testaremtestarem códigos mal escritos.


Obstáculos encontrados

Vou falar brevemente sobre os obstáculos que encontrei para a escrita de testes unitários. Eles não são apenas técnicos, incluindo também fatores humanos.

Códigos difíceis de se testar

O primeiro e principal obstáculo é a dependência direta de implementações com alto acoplamento entre módulos e falta de guiamento por princípios de desenvolvimento. Códigos que não seguem os princípios de Responsabilidade Única (SRP) e Inversão de Dependência (DIP) dos princípios SOLID são códigos difíceis de se testar. Isso ocorre pela dificuldade em se testar funções que tenham diversas dependências e responsabilidades.

Pelo objetivo do teste unitário de testar uma função, ao se deparar com funções grandes o teste unitário se torna muito disperso e bem mais complicado de se atestar sua veracidade. Afinal, ao testar uma função, eu não quero deixar que comportamento anormal de outras afetem a sua execução, sendo necessário utilizar mocks para quaisquer dependências que a função a ser testada tenha. Para permitir isso, precisa-se seguir o princípio de Inversão de Dependência e utilizar interfaces.

Limitação técnica

O segundo obstáculo que encontrei foi a limitação técnica de equipes para a escrita de testes unitários. Não é algo tão difícil de se aprender, mas requer um mínimo de dedicação e um entendimento sincero do que se está fazendo. Não é tão fácil encontrar equipes e projetos que adotem os testes unitários, e isso influencia na dificuldade de se encontrar pessoas que tenham boa familiaridade com eles. Ser humilde e pedir orientação é a melhor saída, junto com os estudos com materiais e cursos.

Fator humano

O terceiro obstáculo que encontrei foi o fator humano. Alguns desenvolvedores se imaginam acima de testes unitários. Crêem que seu código é livre de bugs, e que a mera citação de ter que cobrir seu código de testes lhe parecem uma crítica ou insinuação de má qualidade do seu trabalho. Esses desenvolvedores tentarão sabotar os testes, escrevendo o mínimo de testes possível e com asserções das mais simples possível, não cobrindo de verdade o que precisaria ser coberto, e deixando o código frágil. Já encontrei programadores assim, e a realidade é que a maneira de “tratar” esse problema é ser franco com eles e adotar cobertura de testes e revisão de código.

Prazos curtos

O quarto e último obstáculo são os temidos prazos. Em teoria, todos queremos entregar software de qualidade e com boa cobertura de testes, certo?

Porém, quando um prazo curto é apresentado à equipe, o bom senso quanto ao código frequentemente é abandonado. Não os culpo, já vi de perto e em primeira pessoa essas situações, onde os projetos de testes ficam para trás e viram obsoletos devido a pressões constantes por entregas. Após o período de “tempestade”, em teoria poderíamos voltar aos testes, certo?

O que ocorre é que deparados com a grande quantidade de código já escrito, o caminho mais natural e fácil de seguir é… continue a nadar! E assim é selado o destino de mais uma iniciativa de se utilizar testes unitários em uma equipe de desenvolvimento. R.I.P.


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

Já vi diversos projetos que estavam no hype da escrita de testes unitários, e em pouquíssimos meses tiveram essa prática interrompida por algum dos obstáculos que citei anteriormente. Com as informações que mostrei acima, é importante as equipes avaliarem em qual estado estão em sua adoção desses testes, e se prepararem para superar esses obstáculos.

Em minha sincera opinião, a sua adoção vale e muito a pena! Acho que além de garantir uma maior qualidade do que está sendo entregue, reforça o código contra regressão de bugs que foram corrigidos e cobertos por testes e cria uma super cultura orientada a qualidade e colaboração na equipe.

É isso, até o próximo artigo!

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