Como escrever testes unitários que fazem a diferença?

Neste artigo você vai ver:

Neste artigo, vamos revisar o que são testes unitários e como é um diferencial saber escrevê-los bem. Se você chegou aqui, já é um bom sinal que você se preocupa com a qualidade das suas entregas e entende o quão precioso é investir seu tempo em escrever testes que agregam valor (e vão muito além de uma simples cobertura). 

Como um complemento do conteúdo, também listamos seis recursos que, se aplicados da forma correta, vão contribuir para facilitar a aplicação de melhores práticas.

O que são testes unitários?

Os testes unitários são um dos recursos que nos ajudam a automatizar a validação do comportamento do software que estamos implementando.

A imagem contém uma pirâmide, do lado esquerdo uma seta para cima (indicando o aumento de custo e esforço) e do lado direito uma seta para baixo (indicando diminuição do tempo gasto). A pirâmide está dividida em cinco sessões, sendo de baixo para cima: testes unitários, testes de componentes, testes de integração, testes de API e testes funcionais.

A pirâmide acima dá visibilidade aos vários níveis de testes que poderíamos acrescentar no nosso dia a dia de trabalho, e os unitários estão bem na base. Isso significa que eles são testes baratos (de baixo custo e esforço) e rápidos de serem executados. Normalmente, eles são escritos por pessoas desenvolvedoras e validam o comportamento de pequenas partes isoladas do código, como funções ou métodos.

Sempre que fazemos a construção do artefato do nosso software, esses testes devem ser executados para verificar se as mudanças incrementadas não afetaram o funcionamento das regras de negócio já existentes.

Mas qual é o real impacto dessa decisão de fazer esses testes bem feitos?

Poderíamos escrever um artigo apenas sobre isso, mas para trazer ainda mais consciência sobre essa decisão, podemos citar alguns pontos como:

  • Redução de erros e bugs: testes unitários bem escritos nos ajudam a detectar erros e bugs de forma precoce e conseguimos corrigi-los antes mesmo de chegar para a validação de QA (e principalmente antes de causar problemas para as pessoas usuárias finais).
  • Economia de tempo e recursos: tratar um problema em ambiente produtivo requer muito tempo e também uma observabilidade muito bem implementada para dar visibilidade de onde está a raiz do erro. Identificar os problemas antes de irem para a produção contribui na redução de muitas dores de cabeça (e noites em claro).
  • Facilidade na manutenção: se está difícil testar o código, provavelmente ele está mal escrito. A menor fração do seu código, os métodos, devem ser testados, e para que isso seja feito da melhor forma é preciso que eles tenham responsabilidades únicas (que é um princípio do SOLID, temos um conteúdo que você pode se aprofundar sobre isso… Mas, leia depois de terminar esse aqui, hein?). Ou seja, há uma contribuição na qualidade do seu software e consequentemente ajuda na manutenção.
  • Aumenta a confiança do time: para mim, esse é um dos principais pontos. Me sinto muito mais confiante em realizar alterações e refatorações em códigos que estão bem testados, pois caso eu altere alguma linha que interfira na regra de negócio, os testes unitários vão falhar e evitar que essa alteração chegue às pessoas usuárias.

6 recursos para evoluir a qualidade dos seus testes unitários

Por fim, chegamos na lista de recursos que vão te ajudar a escrever testes unitários ainda melhores:

1. Escolha uma convenção para nomear os testes

Essa convenção contribui na padronização da nomenclatura dos métodos de testes e gera maior facilidade na manutenção. Existem algumas convenções populares, como:

  • when_EstadoDoTeste_Expect_ComportamentoEsperado()

Exemplo: When_AgeEqualsOrGreaterThan18_Expect_ReturnTrue()

  • nomeDoMetodo_EstadoDoTeste_Expect_ComportamentoEsperado()

Exemplo: isAdult_AgeGreaterThan18_Expect_ReturnTrue()

*Esse aqui, eu deixo apenas a observação que, se o nome do método mudar, você terá que refatorar todos os nomes dos métodos.

  • test + Feature sendo testada:

Exemplo: IsAdultIfAgeGreaterThan18()

*Uma variação dessa convenção é remover o prefixo “test” e deixar apenas o nome da feature sendo testada.

  • given_PreCondicoes_When_EstadoDoTeste_Then_ComportamentoEsperado()

Exemplo: given_UsuarioComSaldoInsuficiente_When_TentarRealizarCompra_Then_DeveRetornarErroDeSaldoInsuficiente()

*Nesse caso, é uma abordagem baseada no Behavior-Driven Development (BDD).

2. Organize seus pensamentos ao escrever o teste

Para organizar o pensamento e a codificação na hora de escrever seus testes, você pode utilizar tanto o padrão Given-When-Then quanto o Arrange-Act-Assert. Eles são muito similares. 

Aliás, existem discussões profundas se eles não são a mesma coisa… Até o Robert Martin (Uncle Bob, escritor de vários best-sellers como Código Limpo) deixou seu ponto de vista sobre o tema no tweet abaixo, no qual ele afirma que são a mesma coisa.

Tweet feito por Uncle Bob Martin (@unclebobmartin) no dia 28 de dezembro de 2018. Tradução livre: Todas as formas de testes, sejam Given/When/Then ou Arrange/Act/Assert ou ... são declarações de transição de estado: dado estado A, quanto acontecer o evento X, então vai para o estado B. Portanto, os conjuntos de teste são as transições de estado que descrevem o App como um FSM.

Vou falar sobre cada um deles de forma separada para que você determine sua preferência:

  • Given-When-Then

Esse padrão tem origem no Behavior Driven Development (BDD) e nos ajuda a pensarmos a nível de comportamento em vez de um estado interno.

Given: dado um estado ou situação (que você configurou no teste, e aqui podem entrar seus mocks e inicialização de variáveis);

When: quando eu faço algo ou um evento acontece (quando chamamos o método que estamos testando passando os parâmetros que criamos anteriormente);

Then: então espero um resultado (afirmar) ou interação (adicionar asserções).

Por exemplo:

  • Triple A (Arrange, Act, Assert)

O Triple A é um padrão similar ao anterior que surgiu na comunidade do eXtreme programming, inclusive, é onde Bill Wake escreveu um artigo sobre o tema.

Referenciando o artigo que Wake escreveu, essa é a definição do 3A:

Arrange/Organizar: configure o objeto a ser testado. Podemos precisar cercar o objeto com colaboradores. Para fins de teste, esses colaboradores podem ser objetos de teste (simulações, falsificações, etc.) ou reais.

Act/Agir: agir no objeto (através de algum modificador). Você pode precisar fornecer parâmetros (novamente, possivelmente objetos de teste).

Assert/Asserções: faça afirmações sobre o objeto, seus colaboradores, seus parâmetros e possivelmente (raramente!!) o estado global.

3. Cenários felizes vs cenários de erro

Escrever cenários felizes e de erro em testes unitários é importante, porque eles ajudam a validar diferentes comportamentos do código e garantir que ele esteja funcionando corretamente em diferentes situações.

Os cenários felizes, também chamados de casos de uso ideais, representam o comportamento esperado do código em situações normais de uso. Esses testes são importantes, porque ajudam a validar a lógica principal do código, garantindo que ele esteja funcionando corretamente nos casos em que tudo funciona como esperado.

Já os cenários de erro, ou casos de uso não ideais, representam situações em que o código pode falhar ou apresentar comportamentos inesperados. Estes testes ajudam a validar a capacidade do código de lidar com situações adversas, como entradas inválidas, falta de recursos, interrupções de rede, entre outros. Além disso, eles ajudam a detectar e corrigir erros e falhas no código, garantindo que ele esteja mais robusto e confiável.

Em resumo, a combinação de cenários felizes e de erro em testes unitários é importante para garantir que o código esteja funcionando corretamente em diferentes situações, tanto em situações ideais quanto em situações de exceção. Isso ajuda a aumentar a qualidade do software e reduzir o risco de problemas e erros para pessoas usuárias finais.

4. Explore mais cenários de erro: testes de mutação

Testes de mutação são uma técnica de testes automatizados que são aplicados para verificar a qualidade dos unitários. Basicamente, eles adicionam pequenas alterações no código-fonte, criando assim “mutantes”, que são versões modificadas do código original.

Um mutante poderia ser, por exemplo, a inversão de uma condição, alteração de um operador ou até mesmo a exclusão de uma declaração no código. Depois de colocar esses mutantes, os testes unitários são executados, porém com o objetivo de identificar as falhas introduzidas pelas mutações!

O legal é que se seus testes não detectarem a falha, um relatório será gerado informando que há falha na cobertura de testes e é necessário acrescentar novos testes para cobrir aquele cenário.

É uma forma bem interessante de avaliar a qualidade dos testes e identificar falhas de cobertura. Além disso, é muito fácil acrescentar ele no seu dia a dia. Com Java, por exemplo, basta utilizar o pitest nas dependências e executar um comando maven específico pedindo a cobertura da mutação. 

5. Test Driven Development (TDD)

Uma vez que estiver dominando a escrita dos testes unitários, o Test Driven Development (TDD) poderá agregar muito na sua rotina! TDD é uma prática de desenvolvimento de software que consiste em escrever testes antes de escrever o código, executando os testes e, em seguida, refatorando o código para melhorar a qualidade e atender os requisitos.

No ZupCast, temos um episódio que explica de forma simples e direta sobre a prática (inclusive, eu tive a honra de falar sobre esse assunto). Só clicar aqui para ouvir:

6. Porcentagem de cobertura de testes

Esse ponto foi deixado por último aqui na nossa lista de propósito, geralmente ele é o primeiro ponto quando se é decidido sobre testes unitários em um projeto. Entretanto, há pontos de atenção e boas práticas relacionadas a ele.

Primeiro: a porcentagem de cobertura resolve todos os problemas? Não, pois é muito simples escrever um teste que, infelizmente, não faz nenhuma validação real e já atinge a cobertura combinada. Mas, quando utilizada da forma correta, ela ajuda a elevar a régua da equipe.

Alguns passos são importantes a serem considerados ao utilizar a cobertura de testes:

  • definir objetivos de cobertura de testes (porcentagem a ser alcançada no projeto, e também nas novas contribuições);
  • automatizar o processo de validação da cobertura de testes através de ferramentas;
  • identificar e ter combinado com o time quais são as classes “não-testáveis” e excluí-las da análise da cobertura (atenção: tudo que tiver regra de negócio é importante ser testado).

Conclusão

Desenvolver bons testes unitários é uma prática contínua e esses recursos mencionados acima podem ser nossos aliados. É importante ter em mente que os testes são um investimento na qualidade da nossa entrega e poderão contribuir com códigos mais limpos e de melhor manutenibilidade.

Continue aprendendo com a gente aqui na nossa Central de Conteúdos! Temos artigos sobre back-end, front-end, dados, diversidade, inovação, entre outros temas. Ficou com alguma dúvida? Nossa seção de comentários está disponível para você!

Imagem capa do conteúdo sobre testes unitários, onde uma pessoa branca está em pé, segurando um notebook aberto dentro de um data center.
5ecc128c9deedd561f90700c_monica-ribeiro
Backend Developer
Inspirada em transformar o complexo em algo simples. Sou criadora de conteúdo no Instagram @monicaintech, amo contribuir em comunidades e congelar momentos em fotografias.

Artigos relacionados

Capa do artigo em foto com duas pessoas escrevendo códigos em frente a dois notebooks.
Back-End
Postado em:
Capa com a foto de uma mulher de cabelos trançados de costas de frente para um computador com códigos.
Back-End
Postado em:

Este site utiliza cookies para proporcionar uma experiência de navegação melhor. Consulte nossa Política de Privacidade.