Como evitar a falácia da cobertura de código e escrever código de teste de qualidade (Arrange, Act and Assert)

Diogo Peixoto
5 min readJul 7, 2021

--

Code Coverage

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:

  1. Estamos passando como argumento do mock acoes.findByTicker e proventos.update o matcher any().
  2. Estamos passando como argumento do verify o matcher any().

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.

--

--

Diogo Peixoto
Diogo Peixoto

Written by Diogo Peixoto

Apaixonado por compartilhar, errar, aprender e um pouco de engenharia de software

No responses yet