Ferramentas para testes em GO
Reuni aqui algumas ferramentas e técnicas que usamos nos nossos projetos a fim de tornar nossos testes mais ágeis, rápidos e eficientes.
Separação do package
Iniciamos com um tópico simples: separar os testes em um package próprio sufixado com _test
|
|
|
|
Isso traz uma separação clara dos escopos dos testes e do código de implementação em si, evitando, por exemplo, de receber uma variável disponível no package de implementação dentro dos testes.
Subtest
O Go possibilita rodar testes dentro de testes, o que ele chama de subtest
, com isso, é possível reutilizar um escopo para um teste, facilitando a execução de vários testes.
Um exemplo, seria um método para verificar se um fatura foi paga com o valor correto e dentro da data de vencimento.
|
|
Podemos usar os subtest do GO usando a sintaxe t.Run(name, func(){})
para rodar diversos cenários para validar esse código
|
|
Então podemos adicionar novos cenários de testes no slice scenarios
, evitando ter que reescrever os parses de data, e toda a construção dos Invoice
e Payment
.
Algumas ideias gerais sobre essa abordagem:
- As vezes pode ser difícil criar dentro de um único teste todos os cenários envolvendo muitos mocks, muitos retornos e afins, então, avalie se não vale a pena separar os testes, por exemplo, um para os cenários de sucesso e outro para cenários de falha
- Pela facilidade em criar cenários, explore o máximo possível cenários diferentes, mesmo que esteja fora do escopo da tarefa. O que acontece se eu gerar uma fatura com valor negativo? O código continua funcionando?
- Sempre que um novo bug aparecer, adicione nesses cenários. Por algum motivo, foi descoberto que se a fatura estiver com data “2024-01-01”, o sistema não consegue calcular. Adicione o cenário e corrija o problema. Isso evitará que o problema volte e deixará seus testes mais robustos.
Imaginar diferentes cenários de testes, tornam nosso código mais resiliente ao tempo, principalmente quando pensamos em coisas que podemo nunca acontecer (quais as chances de um cliente gerar uma fatura, receber um desconto de 100% e mesmo assim, ainda tentar pagar a fatura? Mas vai que…)
Sqlmock
Outro exemplo comum de testes que precisamos fazer no dia a dia são os testes de banco de dados. Para simplificar os testes unitários e não precisar da dependência de containers externos ou outros serviços, temos bibliotecas que facilitam esses tipos de testes como o sqlmock. Essa lib é um driver no qual é possível fazer asserts se um sql específico foi executado ou não. É bem simples de configurar, basta passar a conexão o sqlmock como conexão para a biblioteca de banco de dados que já está em uso, no caso daqui, vou usar o gorm com mysql:
|
|
Aqui temos um código que cria um usuário no banco. Para facilitar a leitura está tudo em um único arquivo, mas sempre separe a conexão com o banco das queries em si.
|
|
Já aqui temos o teste em si. Começamos com a criação da conexão do sqlmock, ele retorna a conexão com o banco de dados falso, o mock que podemos interagir para adicionar os expects, e um erro caso exista.
Depois podemos adicionar as queries que esperamos que nosso código irá gerar, no caso, o GORM sempre faz essa verificação da versão do banco, então fazemos um ExpectQuery
desse select, e em seguida, adicionamos o ExpectExec
para a query de inserção do usuário novo. Podemos passar os argumentos esperados e também o retorno do banco (que no caso é o ID (2) dessa inserção e quantas linhas foram alteradas (1)).
Com isso podemos rodar os testes, verificando se as sqls estão rodando conforme o esperado, e sem a necessidade de ter um banco de dados real executando.
Ooze
A última ferramenta do artigo é a que utilizamos para testes de mutação. Usamos o Ooze, com ela podemos testar nossos testes, e assim, termos uma ideia melhor se nossos testes estão pegando bons cenários. Na prática o que os testes de mutação fazem é, rodar todos os testes, então ele altera algo no código do projeto, como por exemplo, trocar um sinal de +
para um sinal de -
, e depois roda os testes novamente. Se os testes continuarem passando, significa que tem algo errado, que não tem teste o suficiente, que verifique essa troca de sinal.
Na própria página do projeto tem uma lista de possíveis “vírus” que são inseridos.
Como a cada nova modificação, os testes precisam ser executados novamente, esses testes tendem a ser mais custosos com relação a processamento, então é interessante colocar eles no final da pipeline, depois dos testes unitários e cobertura.
Outro ponto importante, é ter 100% de cobertura assim os testes de mutação serão mais eficazes. Segue abaixo um exemplo simples de configuração do Ooze:
|
|
Aqui temos um código bem simples, que só verifica se o número é menor que zero e retorna true/false
|
|
Aqui temos os testes, com dois cenários, um para válido e outro para inválido. Se vermos a cobertura dos testes, com go test ./... -coverprofile=cover.out
, podemos ver que está em 100%. Perfeito!
Agora vamos configurar o Ooze. A gente cria uma pasta separada raiz do projeto chamada tests
e cria um arquivo chamado mutation_test.go
(essas foram só convenções que a gente adotou aqui, na real, essas pastas e arquivos podem ter o nome que quiser). Nele teremos o seguinte conteúdo:
|
|
Na primeira linha temos a tag de build para esse código só ser executado/buildado quando a tag existir na linha de comando. Dentro da função temos a configuração de onde estão os arquivos GO que queremos verificar, e qual o threshold, ou o mínimo da relação falha/sucesso que queremos, nesse caso é de 0.75, ou seja, pelo menos 75% dos “mutantes” devem ser mortos pelos nossos testes.
Então é só executar os testes com go test ./... -tags=mutation -v
. O resultado?
Temos 3 mutantes sobreviventes. Se olharmos o output do Ooze, vemos o que foi feito:
O Ooze alterou o código em alguns pontos: ele colocou <=
onde era <
, colocou -2
onde era 0
e colocou 1
onde era 0
. Em cada uma dessas alterações ele rodou os testes novamente, e os testes passaram!
Ou seja, nossos testes não estão pegando todos os possíveis caminhos. Podemos adicionar mais cenários nossos nossos testes:
|
|
E se rodarmos os testes de mutação novamente:
Sucesso!
O Ooze nos ajuda a encontrar esses possíveis problemas de lógica, ou caminhos do código no qual não nos atentamos nos nossos testes.
Então é isso, esse foi o compilado de algumas estratégias que adotamos para melhorar os testes dos nossos códigos GO!