Como evitar a falácia da cobertura de código e escrever código de teste de qualidade (Arrange, Act and Assert)
A importância dos dados
Se uma decisão é baseada em opiniões, minha opinião vence. No entanto, os dados superam minha opinião. Então me traga dados — Jeff Bezos (Fundador da Amazon)
Essa frase foi dita em uma entrevista de Jeff Bezos com o cofundador do LinkedIn Reid Hoffman e está no livro Blitzscaling: The Lightning-Fast Path to Building Massively Valuable Companies [1].
Essa afirmação reflete a necessidade dos executivos e gerentes necessitarem de dados para tomadas de decisões. As performances de pessoas, equipes, aplicações, também são medidas através deles. Por exemplo, capacity — a média da quantidade de pontos entregue numa sprint e tps — quantas transações por segundo uma aplicação pode atender, etc..
No entanto, o dado nada mais é do que um número sem interpretação, uma informação bruta que precisa ser trabalhada. Através do tratamento e da análise do dado bruto, geramos significado prático que serve para apoiar a tomada de decisão [2].
Métricas da vaidade
Recentemente, participei de um projeto que tinha como uma das metas aumentar a qualidade do código, e para isso precisávamos aumentar a cobertura do código de ~50% para ~85%. Estou completamente de acordo com a melhoria contínua do código através das práticas de TDD, Pair Programming, Refactoring, Self-testing code, etc.
No entanto, esse número que perseguíamos nada mais era do que uma métrica da vaidade, importante para a gestão apresentar nas reuniões. Na realidade, não estávamos melhorando a qualidade do nosso código e tampouco escrevendo código que testasse as funcionalidades do nosso software devido a falta de qualidade de nossos testes.
Pois, existe uma sutil diferença entre cobertura de código e percentual do código testado. O primeiro indica que durante a execução da suite de testes (unitários, integração, componentes, etc) x% do código foi executado. E o segundo indica o percentual do código de produção que foi testado pela suite de testes. A diferença entre executar e testar é significativa o suficiente para encontrar ou deixar de encontrar um bug no código.
Arrange, Act e Assert (AAA)
O padrão AAA nos ajuda a criar testes de qualidade dividindo-o em três seções: Arrange, Act e Assert.
Arrange
Nesta seção, que geralmente é a maior, codificamos a preparação da execução do teste. Essa preparação consiste na criação das variáveis que serão passadas no método a ser testado, das variáveis que serão passadas nos métodos que serão mockados ou até mesmo qualquer outra preparação como conexão com base de dados (Caso exista algum passo que é igual para todos os testes, pode-se extrair para um método [3]).
As linhas 5 e 6 inicializam as variáveis que serão utilizadas nas chamadas das duas classes de serviço Acoes
e Proventos
. As linhas 8 e 9 mockam as chamadas destas classes. Agora o nosso teste está pronto para chamar o método a ser testado.
Act
Nesta seção o componente que você quer testar o comportamento deve ser chamado. Ele pode ser uma chamada Rest API, uma página web, uma classe de serviço, etc. No exemplo a seguir, o alvo do nosso teste é classe de serviço service
que recebe um Provento
e retorna um novo provento atualizado (não existe nenhum side effect nesta chamada).
A linha 12 é justamente a chamada do nosso alvo. Como esse teste é bem simples, a seção de Act tem apenas uma linha. Depois de atuarmos em nosso componente alvo a ser testado, nos falta um último passo que as vezes é neglicenciado pelas pessoas: Assert.
Assert
Todas as três partes deste padrão são importantes. No entanto, essa última etapa as vezes é desprezada pelas pessoas, e geralmente, é o que faz a diferença em um bom teste.
Nesta seção vamos verificar se o resultado do nosso teste é exatamente o que esperamos. Podem existir dois tipos de testes: um que retorna um valor ou um objeto, e outro que não retorna nada (void).
Geralmente, para verificar se um componente que não retorna nada foi executado corretamente, checamos se os componentes que ele invoca foram chamados. Isso é possível através das cláusulas verify
do JUnit.
Outros componentes que retornam valor podem ser testados através das cláusulas assert
do JUnit.
A linha 15 verifica se o resultado da execução do método update
da classe service
é igual ao retorno da chamada update
da classe proventos
que foi mockada na linha 9. As linhas 16 e 17 verificam se os métodos mockados das linhas 8 e 9 foram chamados uma única vez.
Com a divisão por seções do teste fica mais simples de saber o que está sendo testado e imaginar qual seria a estrutura do código que está em produção. A seguir o código em produção que está sendo testado.
Bad Practices
Durante o projeto, vi duas situações recorrentes que prejudicavam a qualidade do código, mas, faziam com que a cobertura do código aumentasse.
Ao mockar uma classe, passar os valores dos argumentos usando matcher do Mockito (Mockito.any()) [4].
Caso o objetivo do teste seja validar o fluxo completo de uma operação, é importante testar também se estamos passando os argumentos corretos para os métodos que são chamados dentro do nosso método alvo. No exemplo anterior, estamos passando o valor de provento.acao().ticker()
para o método findByTicker
de acoes
.
Não testar o resultado esperado
Se você não testar o resultado esperado da chamada do componente, o teste não validará o código de produção, estará apenas passando por ele. E como consequência, a ferramenta de análise estática de código vai indicar que o código de produção foi executado no momento de rodar os testes.
O código anterior é um exemplo de como NÃO devemos testar um código. Nesse teste, estamos queremos validar o fluxo completo do update
do componente service
. No entanto:
- Estamos passando como argumento do mock
acoes.findByTicker
eproventos.update
o matcherany()
. - Estamos passando como argumento do
verify
o matcherany()
.
Caso modifiquemos a lógica do código em produção e sem querer inserirmos um bug, o nosso teste não vai falhar, no entanto, deveria. Esse tipo de teste é chamado teste de mutação que consiste em mudar o código para fazer o teste falhar [5].
No código anterior, mudamos o argumento da chamada acoes.findByTicker
de provento.acao().ticker()
para "DEU RUIM HEIN?"
e o nosso teste continuou funcionando. Se ao invés de termos passado o matcher any()
, passamos o argumento esperado em nosso teste (provento.acao().ticker()
), a nossa suite de testes indicaria o bug.
Conclusão
Ambos os testes apresentados update_ShouldUpdate_Good
e update_ShouldUpdate_Bad
possuem a mesma cobertura de código. No entanto, apenas o primeiro valida corretamente o comportamento do componente.
Na criação de testes, é importante entender o que significa a cobertura de código e não focar simplesmente nela, pois, como vimos, um código pode estar 100% coberto e não estar sendo validado corretamente. Com o padrão AAA fica mais fácil de criar testes de qualidade que verifiquem o funcionamento correto do código de produção.
Pois, a diferença entre um teste bem escrito e um mal escrito é justamente identificar um bug na fase de testes ao invés de identificá-lo em produção.
ps: Os testes devem aplicar os mesmos princípios de código limpo de produção. Pois, eles são uma forma de documentar o comportamento da aplicação.
Referências
[1] https://www.amazon.com.br/Blitzscaling-Lightning-Fast-Building-Massively-Companies-ebook/dp/B07BBR9KCY/
[2] https://www.knowsolution.com.br/diferenca-dado-e-informacao/
[3] https://automationpanda.com/2020/07/07/arrange-act-assert-a-pattern-for-writing-good-tests/
[4] https://site.mockito.org/javadoc/current/org/mockito/ArgumentMatchers.html
[5] https://searchitoperations.techtarget.com/definition/mutation-testing