Como criar testes unitários para a linguagem go?

--

_________________________________________________________________

Se tiver interesse em aprender a base da linguagem go, recomendo este curso feito pela Ellen Körbes.

Curso: Aprenda GoRepositório do Curso

_________________________________________________________________

Para que serve testes unitários?

Testes unitários, assim como qualquer teste automatizado não servem principalmente para verificar se uma função específica está funcionando, mas sim para garantir que sua aplicação continue funcionando após alguma alteração no código.

_________________________________________________________________

O que são oráculos?

O oráculo é um instrumento que vem sendo largamente utilizado como uma forma de comparar resultados esperados para determinados casos de teste aplicados ao sistema em teste. É como um conjunto de tuplas onde para cada entrada possıvel, uma saída esperada é associada.

Referência

_________________________________________________________________

Setup do projeto:

É necessário apenas ter a linguagem instalada na máquina mas se ainda não tem instalado, aqui está o link da linguagem go:

Golang

Com o compilador do go instalado rode este comando no terminal:

go version

O output será parecido com este:

go version go1.20 linux/amd64

Agora que já sabe qual versão do go está instalado basta criar um Repositório no Github e clonar ele para sua máquina.

Entre na pasta do repositório que você clonou e crie um arquivo chamado go.mod colocando as seguintes informações:

module github.com/username/repositoryName

go 1.20

Troque o 1.20 pela versão do compilador que você tem instalado e substitua o campo username pelo seu username do github e o campo repositoryName pelo nome do seu repositório.

_________________________________________________________________

Iniciando o estudo de testes unitários:

Bom, para testar usaremos a lib Testing, e como falei anteriormente ela já vem junto da linguagem. O exemplo que vamos utilizar será de como testar uma simples calculadora.

Primeiro vamos criar as Funções da calculadora em um arquivo chamado calc.go:

Obs: substitua o nome do package pelo nome do seu repositório no github

package unitTestingGolang

func Add(number, number2 int)int{
return number + number2;
}

func Sub(number, number2 int)int{
return number - number2;
}

func Mult(number, number2 int) int{
return number * number2;
}

func Div(number, number2 int) int{
return number / number2;
}

Agora que já criamos as funcionalidades vamos criar testes unitários para cada função em um arquivo chamado calc_test.go:

Você pode escrever assim:

Obs: substitua o nome do package pelo nome do seu repositório no github

//o package que vai ser testado
package unitTestingGolang

//importando a Lib Testing
import "testing"

// Está função testa a função Add
func TestAdd(test *testing.T){
/*
Aqui vemos uma variável chamada result que recebe o retorno da função Add
que tem que ser a soma dos dois números passados por parâmetro.
*/
result := Add(2,2);
//Na variável expected atribuímos o valor que é esperado da variável result
expected := 4;
/*
Aqui é o oráculo que compara se o valor encontrado em result é diferente
do esperado que foi definido na variável expected
*/
if result != expected{
/*
A função Errorf é quem imprime o erro mostrando o que foi recebido
pela variável result e o que era esperado.
*/
test.Errorf("Result: %d, Expected: %d", result, expected);
}
}
// Está função testa a função Sub
func TestSub(test *testing.T){
/*
Aqui vemos uma variável chamada result que recebe o retorno da função Sub
que tem que ser a subtração dos dois números passados por parâmetro.
*/
result := Sub(4,2);
//Na variável expected atribuímos o valor que é esperado da variável result.
expected := 2;

/*
Aqui é o oráculo que compara se o valor encontrado em result é diferente
do esperado que foi definido na variável expected.
*/
if result != expected{
/*
A função Errorf é quem imprime o erro mostrando o que foi recebido
pela variável result e o que era esperado.
*/
test.Errorf("Result: %d, Expected: %d", result, expected);
}
}
//Está função testa a função Mult
func TestMult(test *testing.T){
/*
Aqui vemos uma variável chamada result que recebe o retorno da função Mult
que tem que ser a multiplicação dos dois números passados por parâmetro.
*/
result := Mult(4,5);
//Na variável expected atribuímos o valor que é esperado da variável result
expected := 20;
/*
Aqui é o oráculo que compara se o valor encontrado em result é diferente
do esperado que foi definido na variável expected
*/
if result != expected{
/*
A função Errorf é quem imprime o erro mostrando o que foi recebido
pela variável result e o que era esperado.
*/
test.Errorf("Result: %d, Expected: %d", result, expected);
}
}

//Está função é a que testa a função Div
func TestDiv(test *testing.T){
/*
Aqui vemos uma variável chamada result que recebe o retorno da função Div
que tem que ser a divisão dos dois números passados por parâmetro.
*/
result := Div(4,2);
//Na variável expected atribuímos o valor que é esperado da variável result
expected := 2;
/*
Aqui é o oráculo que compara se o valor encontrado em result é diferente
do esperado que foi definido na variável expected
*/
if result != expected{
/*
A função Errorf é quem imprime o erro mostrando o que foi recebido
pela variável result e o que era esperado.
*/
test.Errorf("Result: %d, Expected: %d", result, expected);
}
}

Quer saber mais sobre as funções do package testing? leia a documentação oficial ou leia este README em português.

Neste primeiro exemplo criamos os Teste em funções separadas sendo que cada função possui um oráculo que neste caso é o:

if result != expected{
test.Errorf("Result: %d, Expected: %d");
}

Aqui chamamos a função Errorf() == func (c *T) Errorf(format string, args ...any) que vem de test que é um ponteiro para testing.T.

A função Errorf é quem imprime o erro mostrando o que foi recebido pela variável result e o que era esperado que a variável result retornasse que foi definido na variável expected. Se quiser entender melhor está função e as demais leia a documentação.

Outra forma de escrever é assim:

Obs: substitua o nome do package pelo nome do seu repositório no github

package unitTestingGolang

import "testing"

// Dentro dessa função estarão todos os testes unitários
func TestCalc(test *testing.T){
/*
Está variável recebe uma função anônima que tem como paramêtro o test
que é um ponteiro para testing.T, o result e o expected
que são o que o oráculo irá comparar
*/
checkExpectedResult := func(test *testing.T,result,expected int){
/*
Segundo a documentação oficial a função Helper marca a função de chamada
como uma função auxiliar de teste.
*/
test.Helper();
/*
Aqui é o oráculo que compara se o valor encontrado em result é diferente
do esperado que foi definido na variável expected
*/
if result != expected{
/*
A função Errorf é quem imprime o erro mostrando o que foi recebido
pela variável result e o que era esperado.
*/
test.Errorf("\nResult: %d\nExpected: %d",result,expected);
}
}
// Aqui utilizamos Run que executa f como um subteste de t chamado name.
test.Run("Add 2 numbers",func(test *testing.T){
/*
Aqui vemos uma variável chamada result que recebe o retorno da função Add
que tem que ser a soma dos dois números passados por parâmetro.
*/
result := Add(50,100);
//Na variável expected atribuímos o valor que é esperado da variável result
expected := 150;
/*
Aqui é o Oráculo que realiza a comparação
de acordo com os parâmetros recebidos
*/
checkExpectedResult(test,result,expected);
})
// Aqui utilizamos Run que executa f como um subteste de t chamado name.
test.Run("Sub 2 numbers",func(test *testing.T){
/*
Aqui vemos uma variável chamada result que recebe o retorno da função Sub
que tem que ser a subtração dos dois números passados por parâmetro.
*/
result := Sub(43,1);
//Na variável expected atribuímos o valor que é esperado da variável result
expected := 42;
/*
Aqui é o Oráculo que realiza a comparação
de acordo com os parâmetros recebidos
*/
checkExpectedResult(test,result,expected);
})
// Aqui utilizamos Run que executa f como um subteste de t chamado name.
test.Run("Mult 2 numbers",func(test *testing.T){
/*
Aqui vemos uma variável chamada result que recebe o retorno da função Mult
que tem que ser a multiplicação dos dois números passados por parâmetro.
*/
result := Mult(5,5);
//Na variável expected atribuímos o valor que é esperado da variável result
expected := 25;
/*
Aqui é o Oráculo que realiza a comparação
de acordo com os parâmetros recebidos
*/
checkExpectedResult(test,result,expected);
})
// Aqui utilizamos Run que executa f como um subteste de t chamado name.
test.Run("Div 2 numbers", func(test *testing.T){
/*
Aqui vemos uma variável chamada result que recebe o retorno da função Div
que tem que ser a divisão dos dois números passados por parâmetro.
*/
result := Div(40,2);
//Na variável expected atribuímos o valor que é esperado da variável result
expected := 20;
/*
Aqui é o Oráculo que realiza a comparação
de acordo com os parâmetros recebidos
*/
checkExpectedResult(test, result,expected);
})
}

Quer saber mais sobre as funções do package testing? leia a documentação oficial ou leia este README em português.

Neste exemplo nos criamos apenas uma função de teste chamada TestCalc(test *testing.T) e dentro dela rodamos todos os testes da função calculadora utilizando um oráculo:

checkExpectedResult := func(test *testing.T,result,expected int){

test.Helper();

if result != expected{
test.Errorf("\nResult: %d\nExpected: %d",result,expected);
}
}

A função Helper, segundo a documentação oficial ela marca a função de chamada como uma função auxiliar de teste. Ao imprimir informações de arquivo e linha, essa função será ignorada. Helper pode ser chamado simultaneamente de múltiplas goroutines. se não sabe o que são goroutines estude pelo site oficial:

Clique aqui para acessar o site oficial

Podemos analisar que para criar os testes unitários é muito simples.

Exemplo:

test.Run("Descriçao do que esta sendo testado",func(test *testing.T){
result := NomeDaFunçaoQueVaiSerTestada(10,10);
expected := 20 // o que a funçao tem que retornar
checkExpectedResult(test,result,expected);
})

Aqui utilizamos a função Run que vem da variável test que é um ponteiro para testing.T que segundo a documentação oficial: func (t *T) Run(name string, f func(t *T)) bool

Run executa f como um subteste de t chamado name. Ele executa f em uma goroutine separada e bloqueia até que f retorne ou chame t.Parallel para se tornar um teste paralelo.

Run informa se f foi bem-sucedido (ou pelo menos não falhou antes de chamar t.Parallel). Run pode ser chamado simultaneamente de várias goroutines, mas todas essas chamadas devem retornar antes que a função de teste externa para t retorne.

Como rodar os Testes?

Para rodar os testes e ver se passaram ou não, abra uma aba no terminal e utilize o comando abaixo:

go test -v

O primeiro Test tem que gerar um output parecido com este:

=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestSub
--- PASS: TestSub (0.00s)
=== RUN TestMult
--- PASS: TestMult (0.00s)
=== RUN TestDiv
--- PASS: TestDiv (0.00s)
PASS
ok github.com/gabrielluizsf/unittestinggolang 0.004s

O segundo Test vai sair um output parecido com este:

=== RUN   TestCalc
=== RUN TestCalc/Add_2_numbers
=== RUN TestCalc/Sub_2_numbers
=== RUN TestCalc/Mult_2_numbers
=== RUN TestCalc/Div_2_numbers
--- PASS: TestCalc (0.00s)
--- PASS: TestCalc/Add_2_numbers (0.00s)
--- PASS: TestCalc/Sub_2_numbers (0.00s)
--- PASS: TestCalc/Mult_2_numbers (0.00s)
--- PASS: TestCalc/Div_2_numbers (0.00s)
PASS
ok github.com/gabrielluizsf/unittestinggolang 0.003s

Como podemos perceber utilizar o segundo exemplo realizamos o teste 0.001 mais rápido, sendo mais rápido e mais intuitivo utilizá-lo porque você não precisa ficar repitindo em todo teste:

if result != expected{
test.Errorf("Result: %d, Expected: %d", result, expected);
}

Você apenas chama a variável checkExpectedResult enviando os dados solicitados pelo parâmetro da função anônima:

checkExpectedResult(test, result,expected);

Agora vamos simular uma situação em que os testes quebrem para ver o que acontece. Digamos que o programador estava distraido e trocou todos os simbolos das funções vamos ver qual seria a output dos testes:

O primeiro exemplo de Teste gera um output parecido com este:

=== RUN   TestAdd
calc_test.go:10: Result: 1, Expected: 4
--- FAIL: TestAdd (0.00s)
=== RUN TestSub
calc_test.go:18: Result: 6, Expected: 2
--- FAIL: TestSub (0.00s)
=== RUN TestMult
calc_test.go:27: Result: -1, Expected: 20
--- FAIL: TestMult (0.00s)
=== RUN TestDiv
calc_test.go:36: Result: 8, Expected: 2
--- FAIL: TestDiv (0.00s)
FAIL
exit status 1
FAIL github.com/gabrielluizsf/unittestinggolang 0.008s

O segundo exemplo de Teste vai gerar um output parecido com este:


=== RUN TestCalc
=== RUN TestCalc/Add_2_numbers
calc_test.go:52:
Result: 0
Expected: 150
=== RUN TestCalc/Sub_2_numbers
calc_test.go:58:
Result: 44
Expected: 42
=== RUN TestCalc/Mult_2_numbers
calc_test.go:63:
Result: 0
Expected: 25
=== RUN TestCalc/Div_2_numbers
calc_test.go:68:
Result: 80
Expected: 20
--- FAIL: TestCalc (0.01s)
--- FAIL: TestCalc/Add_2_numbers (0.00s)
--- FAIL: TestCalc/Sub_2_numbers (0.00s)
--- FAIL: TestCalc/Mult_2_numbers (0.00s)
--- FAIL: TestCalc/Div_2_numbers (0.00s)
FAIL
exit status 1
FAIL github.com/gabrielluizsf/unittestinggolang 0.019s

Ou seja a primeira forma falha mais rápido que a segunda então é um trade-off. Mas se você quer que os testes passem mais rápido e quer entender melhor seu código use a segunda forma de fazer.

Espero ter ajudado de alguma forma :)

Para mais exemplos de Testes clique aqui .

Github

--

--