Testes: Boas práticas e patterns
Atualmente, existem diversas bibliotecas que nos ajudam na escrita de testes. Porém, sem disciplina esses testes podem ser difíceis de gerenciar e ficam sujeitos à frequentes atualizações. Ao longo desse artigo, você encontrará uma série de dicas fundamentais sobre como utilizar padrões e boas práticas na escrita dos seus testes.
Mantenha seus testes consistentes
Use Arrange, Act e Assert (AAA) — uma forma de organizar seus testes em três partes fundamentais:
- Arrange: É a primeira parte fundamental do teste. Nessa etapa você cria os objetos necessários para executar seu teste, faz o mock de APIs, services, etc. É a preparação para o “Act”.
- Act: Executa o código que você quer testar, normalmente uma simples chamada de método.
- Assert: Verifica se o código executado se comportou da forma que era esperada. Isso envolve inspecionar o valor retornado pelo código executado ou o estado de algum objeto envolvido no teste.
Vamos usar como exemplo uma conta bancária em que podemos fazer as seguintes operações: depósito, saque e consulta de saldo.
Podemos demonstrar o uso de AAA em um pequeno teste para verificar se está tudo certo ao realizar um simples depósito.
OBS.: As linhas em branco separando cada parte do teste (Arrange, Act e Assert) são indispensáveis para ajudar a você entender o teste mais rapidamente e consequentemente, melhorar a legibilidade do código.
Teste comportamentos e não métodos
Quando você escreve testes, o foco deve ser sempre no comportamento da sua classe e não em métodos individualmente.
Quando escrevemos testes unitários, precisamos de uma visão mais abrangente, já que estamos testando diversos comportamentos de uma classe e não seus métodos individualmente.
Para entender melhor, no exemplo que vimos anteriormente, nós escrevemos o teste para um simples depósito, no método makeSingleDeposit. Esse teste inclui o uso dos métodos getBalance e makeDeposit da nossa classe. Dessa forma, testamos o comportamento da classe quando um simples depósito é realizado.
Um exemplo semelhante pode ser visto a seguir, no qual testamos o comportamento da classe para múltiplos depósitos.
OBS.: Como estamos falando de uma conta bancária, é recomendado o uso do BigDecimal para trabalhar com números relacionados a dinheiro, ao invés de usar Double ou Float. Veja mais detalhes em: http://www.javapractices.com/topic/TopicAction.do?Id=13
Documente seus testes com nomes consistentes
Quanto mais você combinar casos em um único teste, mais genéricos os nomes do testes podem se tornar. Um método chamado testar não informa a ninguém sobre o propósito do teste.
Em nosso exemplo (mostrado anteriormente), podemos renomear o método makeMultipleDeposits para makeMultipleDepositsIncreaseBalanceBySumOfDeposits, um nome que descreve melhor o teste que está sendo realizado.
Se os outros (ou você mesmo) tiverem dificuldade em entender o que o teste está fazendo, não basta apenas adicionar comentários. Comece melhorando o nome do teste. Além disso, seguem mais algumas dicas:
- Utilize nomes relacionados à nomes de variáveis locais do seu teste
- Altere o nome dos testes para nomes que descrevem em detalhes o que o teste faz, em vez de introduzir comentários explicativos
- Divida grandes testes em partes, em pequenos testes mais focados
- Dê preferência pelos métodos de asserção do Hamcrest. Eles fornecem mensagens mais detalhadas sobre as falhas dos testes.
FIRST —Características de “Bons Testes”
Não é novidade que os testes unitários trazem benefícios significativos quando aplicados com cuidado. Porém, vale lembrar que os testes fazem parte dos códigos que você deve escrever e manter. Diversos problemas podem fazer você e sua equipe perderem muito tempo (e muitas vezes sono), como por exemplo:
- Testes que falham esporadicamente
- Testes que demoram muito para executar
- Testes que não trazem nenhum benefício
- Testes que se associam muito pouco à implementação, o que significa que
mudanças quebram muitos testes de uma só vez - Testes complicados que demandam vários passos de configuração
- Testes que são insuficientes para cobrir o código
Podemos evitar muitas dessas armadilhas adotando o FIRST:
- [F]ast — mantenha seus testes rápidos diminuindo as dependências nos códigos
- [I]solated — deve ser possível executar qualquer teste em qualquer ordem
- [R]epeatable — um teste deve produzir os mesmos resultados sempre que você o executar. Para isso, você deve isolá-lo de qualquer dependência externa que não esteja sob seu controle direto.
- [S]elf-validating — Os testes não são testes, a menos que afirmem que as coisas ocorreram conforme o esperado. Você escreve testes unitários para economizar seu tempo e não gastar mais do seu tempo. A verificação manual dos resultados dos testes é um processo demorado que também pode introduzir mais riscos. É necessário automatizar qualquer dependência externa que seu teste requer.
- [T]imely — Escreva testes em tempo hábil, pois quanto mais você adiar a análise em seu código com testes unitários, maiores serão as chances de defeitos com os quais você precisará lidar.
Escrever testes unitários requer um investimento considerável em tempo. Embora seus testes possam compensar esse investimento, todos os testes que você escreve são códigos que você deve manter. Proteja esse investimento garantindo que seus testes mantenham alta qualidade. Use o FIRST para lembrá-lo das características de um teste de qualidade.
“Patterns” para um código mais legível, compreensível e testável
Antes de começar a falar sobre patterns, vamos retomar nosso exemplo da conta bancária (classe BankAccount) e adicionar os seguintes atributos:
- accountNumber: Número da conta
- accountType: Tipo de conta
- owner: Nome do titular
- interestRate: Taxa de juros
Aparentemente, não há nada de errado com o construtor da nossa classe, certo? Porém, se tivermos vários argumentos consecutivos do mesmo tipo, é fácil de trocá-los acidentalmente. Observe o exemplo a seguir, onde o valor da taxa de juros foi acidentalmente trocado pelo saldo da conta.
Como o compilador não considera isso como um erro, ele pode se manifestar como um problema quando o programa tiver em execução, e o erro descoberto somente após uma árdua tarefa de depuração. Além disso, adicionar mais parâmetros no construtor resulta em um código mais difícil de ler. Se tivéssemos 10 parâmetros diferentes, seria muito difícil identificar o que cada um representa, olhando rapidamente para o nosso construtor. Para piorar, alguns desses valores podem ser opcionais, o que significa que precisaremos criar diversos construtores “sobrecarregados” para lidar com todas as combinações possíveis, ou teremos que passar nulos para o nosso construtor (não faça isso!!).
“Ah, então é só criar um construtor sem argumentos e passar todos os valores via setters”
Essa parece ser a solução do problema, mas e se alguém esquecer de chamar algum método setter? Novamente, o compilador não veria problema nenhum. Em geral, temos dois grandes problemas para resolver:
- Diversos argumentos no construtor
- Estado incorreto de objetos
Então, como vamos resolver? Uma boa alternativa é o padrão Builder ou “The Builder Pattern”!
The Builder Pattern
O padrão Builder nos permite escrever código compreensível e legível para configurar objetos complexos. Normalmente consiste em utilizar uma implementação com fluent interface para criar os objetos. Nós substituímos o construtor da classe BankAccount por um construtor privado e criamos uma classe interna (que chamamos de Builder) para construir o objeto. Observe a implementação abaixo.
Assim, podemos criar novos objetos de uma forma mais legível, utilizando o Builder que construímos para a classe BankAccount. Veja o exemplo a seguir:
“Certo. Mas e como esse tal de Builder aí me ajuda nos testes?”
Esse padrão nos ajuda a deixar de fora alguns valores irrelevantes em determinados testes. O nome do titular da conta ou o tipo de conta, são valores irrelevantes em um teste cujo objetivo é verificar se os depósitos estão sendo realizados corretamente ou não. Utilizando o Builder, podemos construir o objeto apenas com os valores relevantes para o nosso teste. Veja o exemplo abaixo:
Usar o padrão Builder pode trazer grandes melhorias para seus testes:
- Expressividade: Ao passar explicitamente apenas os dados necessários, melhoramos o valor de nossos testes de unidade como uma forma de documentação. Apenas olhando para o teste, você pode determinar o que o método faz.
- Flexibilidade: Ao desacoplar o teste do construtor, garantimos que as alterações futuras não interrompam nossos testes de unidade existentes. Isso é importante por motivos de manutenção (você não quer entrar e alterar muitos testes devido a algumas alterações no código, certo?).
- Confiabilidade: Como nosso teste unitário é flexível em relação a alterações, você não precisará modificá-lo com frequência. Em geral, um teste unitário fica mais confiável quando amadurece. Para imaginar isso, você pode comparar o efeito de um teste unitário com falha que você acabou de escrever com um que foi escrito meses atrás. Um teste de unidade recentemente escrito que falha pode ter muitas razões (um erro no teste, algum código que ainda não foi escrito, etc.). Por outro lado, um teste que está funcionando há meses, mas de repente falha, é mais preocupante e definitivamente indica um problema com o novo código.
Veja mais detalhes sobre Build Pattern no livro Effective Java de Joshua Bloch.
Uma abordagem mais detalhada sobre testes unitários, boas práticas e muito mais, podem ser encontrados no livro Pragmatic Unit Testing in Java 8 with JUnit.
Se você é desenvolvedor Android, não deixe de ler o guia Testing Apps on Android.
Lembre-se sempre de Boas Práticas e Patterns na hora de construir seus testes!
Se esse artigo agregou alguma coisa em seu conhecimento, não deixe de compartilhar! Obrigado por ler até aqui!😁👍🏻