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:

Press enter or click to view image in full size

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:

Press enter or click to view image in full size

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.

Press enter or click to view image in full size

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.

Press enter or click to view image in full size

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