[Dica rápida] Criando teste de mutação com Golang
Os testes unitários são parte essencial no desenvolvimento de software, garantindo que funcionalidades e componentes estejam funcionando conforme esperado. Porém, ter uma boa cobertura de testes não necessariamente significa ter testes eficazes.
Para validar a eficácia dos testes, surge uma técnica chamada teste de mutação.
Neste post, vamos explorar o que é o teste de mutação e como implementá-lo em Go.
Bom, teste de mutação consiste em modificar o código de forma deliberada para avaliar a capacidade dos testes unitários em detectar erros. Esse processo envolve fazer alterações (ou “mutações”) no código-fonte e executar os testes para ver se eles falham conforme o esperado.
Essas mutações simulam possíveis erros que um desenvolvedor poderia cometer, como troca de operadores (por exemplo, +
por -
), inversão de condições, alteração de constantes, entre outros.
O objetivo é verificar se os testes identificam essas modificações, garantindo que cobrem cenários relevantes e são realmente eficazes.
Para que você possa ter um melhor entendimento sobre este assunto, vejamos um exemplo prático. Para isso, imagine o seguinte cenário:
Você tem um método que calcula o lucro de um investimento, considerando o valor inicial e o valor final do investimento.
Esse método recebe dois parâmetros: o valor inicial e o valor final. A ideia é verificar se houve lucro e, caso positivo, retornar o valor desse lucro.
A seguir você tem um trecho de código demonstrando este método:
package investment
func CalculateProfit(initial, final float64) float64 {
if final > initial {
return final - initial
}
return 0
}
Executando ele nós temos o seguinte resultado:
Até aqui OK né? passamos dois parametros simples, o nosso código analisou e retornou o resultado.
Vamos agora criar o nosso teste unitário, para verificar as possibilidades de lucro ou prejuizo:
package investment
import "testing"
func TestCalculateProfit(t *testing.T) {
tests := []struct {
initial, final, expected float64
}{
{100, 200, 100}, // lucro positivo
{200, 100, 0}, // sem lucro
{100, 100, 0}, // sem lucro
}
for _, tt := range tests {
profit := CalculateProfit(tt.initial, tt.final)
if profit != tt.expected {
t.Errorf("CalculateProfit(%v, %v): expected %v, got %v", tt.initial, tt.final, tt.expected, profit)
}
}
}
Aqui nós temos um array com 3 possibilidades:
- A primeira retornando um cenário positivo de lucro;
- A duas seguintes estão retornando um cenário negativo onde não tivemos lucro.
Executando os testes notem que esta funcionando corretamente:
Até aqui tudo tranquilo né?
Vejamos agora como esta a cobertura do nosso código. Para isso, execute o seguinte comando no seu terminal: go test -cover ./…
Resultado:
Note que alem dos testes estarem funcionando corretamente, estamos com um cobertura boa.
É aqui que entra o nosso teste de mutação. Ao executá-lo, ele detecta todos os pontos no código que podem ser modificados e aplica mutações nesses locais.
Vamos configurar o nosso ambiente para ele como ele funciona na prática.
Abra um terminal no seu computador e execute o seguinte comando nele:
go install github.com/zimmski/go-mutesting/cmd/go-mutesting@latest
Agora execute o comando go-mutesting no seu terminal para verificar se ele foi instalado corretamente.
Caso o seu retorno seja o mesmo da imagem a seguir, o seu ambiente esta OK.
Agora para rodar o teste de mutação, execute o seguinte comando no seu terminal: go-mutesting investiment.
Obs.: eu executei o comando no diretorio do meu pacote, caso o seu esteja em um outro caminho, basta alterar o local para executar o teste.
Bom, executando este comando no código apresentado acima, nós temos o seguinte resultado:
PASS "/var/folders/8_/k1fm88l93q978ncwnjpl0rzr0000gn/T/go-mutesting-2753113683/investiment/investment.go.0" with checksum c488d35f75c6afd6493fa44119f2addf
--- investiment/investment.go 2024-11-07 21:35:57
+++ /var/folders/8_/k1fm88l93q978ncwnjpl0rzr0000gn/T/go-mutesting-2753113683/investiment/investment.go.1 2024-11-07 21:48:34
@@ -1,7 +1,7 @@
package investment
func CalculateProfit(initial, final float64) float64 {
- if final > initial {
+ if final >= initial {
return final - initial
}
return 0
FAIL "/var/folders/8_/k1fm88l93q978ncwnjpl0rzr0000gn/T/go-mutesting-2753113683/investiment/investment.go.1" with checksum 0dec43206d47970428bfb8d06df72a43
The mutation score is 0.500000 (1 passed, 1 failed, 0 duplicated, 0 skipped, total is 2)
Ao analisar o retorno, note que um dos testes passou, enquanto outro falhou devido à mutação aplicada. O relatório também destaca a alteração que foi utilizada para provocar a falha no teste, permitindo-nos identificar exatamente como a mutação impactou o comportamento do código.
Para corrigir este fluxo podemos simplemente atualizar o nosso teste unitário com este trecho de código:
if profit != tt.expected || profit == tt.expected {
t.Errorf("CalculateProfit(%v, %v): expected %v, got %v", tt.initial, tt.final, tt.expected, profit)
}
Executando novamente o nosso teste de mutação, note que ele não retorna nenhum erro.
Note como que um simples teste pode nos deixar mais confiantes nos nossos testes :)
Bom, com isso finalizo mais este post, espero que tenham gostado até a próxima pessoal.