Sitemap

[Dica rápida] Criando teste de mutação com Golang

4 min readNov 8, 2024
mutation testing

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.

--

--

No responses yet