Testes
Introdução
Escrever testes para um código é parte essencial do desenvolvimento de aplicações robustas. É extremamente importante sabermos testar a sua validade, conseguir fazer alterações em código existente e saber se elas afetarão demais partes do sistema.
Existem diferentes tipos de testes. Aqui, focaremos em testes unitários.
Em Go, tudo que é necessário para testar já está disponível na biblioteca padrão. O pacote que provê o essencial se chama testing
.
Para executar os testes presentes na aplicação, usamos o comando go test
.
Desenvolvimento orientado a testes
Vamos aproveitar e introduzir a prática do TDD (do inglês Test Driven Development). Vamos abordar apenas o suficiente para explicar testes em Go, mas existem diversos sites, vídeos e livros publicados sobre TDD que você pode buscar posteriormente caso queira saber mais sobre.
A ideia do TDD é que trabalhemos em ciclos:
Primeiro escrevemos um teste que inicialmente irá falhar, tendo em vista que o código ainda não foi escrito
Implementamos a funcionalidade com o mínimo de código para o teste passar
Refatoramos para eliminar redundância e melhorar a legibilidade
Como exemplo, nossa tarefa será escrever uma calculadora (ou melhor, calculadora que faça apenas somas 😆).
Desenvolvendo uma calculadora* usando TDD
Como primeiro passo, começamos a planejar o que o primeiro teste precisa:
Precisamos de uma função que faça a soma de dois algarismos;
Quando chamarmos esta função, queremos “guardar” o seu retorno em uma variável;
Queremos então comparar o valor recebido da função com o valor que esperávamos.
Passo 1: Escrevendo o teste
Vamos ao teste: criaremos o arquivo calculadora_test.go
e representaremos no código o que foi descrito acima
package calculadora
import "testing"
func TestAdicione(t *testing.T) {
obtive := Adicione(3, 4)
espero := 7
if obtive != espero {
t.Errorf("espero '%d', mas obtive '%d'", espero, obtive)
}
}
O que aconteceu até aqui? Vamos explicar…
Primeiro, criamos um arquivo chamado calculafora_test.go
:
Nomeamos o arquivo terminando com _test.go. Assim ele será ignorado quando o executável da aplicação for criado, mas será incluído quando o comando go test for chamado.
Em seguida, criamos a função que é o teste em si:
O nome da função segue o padrão
TestXxx
: Xxx serve para identificar o nome da função que está sendo testada e deve iniciar com letra maiúscula.Como parâmetro, teremos a variável
t
que é um ponteiro paratesting.T
, que por sua vez é um tipo fornecido pela biblioteca de testes do Go, que serve para gerenciar o estado do teste e fornecer suporte à formatação de mensagens.
Depois disso, começamos a implementar o teste propriamente dito:
Caso o valor não seja o esperado, utilizamos um dos métodos que
testing.T
fornece para formatar e imprimir uma mensagem de erro em testes - comot.Errorf()
, que usamos nesse exemplo.
Pronto, agora vamos rodar o teste usando o seguinte comando:
go test
E a seguinte mensagem será mostrada:
./calculadora_test.go:6:12: undefined: Adicione
Ótimo! Era isso mesmo que esperávamos: a função Adicione()
ainda não foi implementada, por isso a mensagem de erro indica que Adicione é "undefined".
Seguimos em frente e a adicionamos a um arquivo chamado calculadora.go
, que criaremos no mesmo pacote que o teste está e adicionamos o mínimo de código a fim de compilar.
package calculadora
func Adicione(x, y int) int {
return 0
}
Agora, ao rodarmos o comando go test
novamente, o erro é outro. Nos livramos do erro de compilação, mas agora recebemos um erro relacionado ao teste:
calculadora_test.go:9: espero '7', mas obtive '0'
Passo 2: Fazendo o teste passar
Agora sim, vamos para o próximo passo, que é fazer o teste passar. E qual é o passo mais rápido para fazê-lo executar com sucesso?
Pode causar estranhamento, mas, nessa etapa, o passo que buscamos é simplesmente substituir o zero pelo resultado esperado no único caso de testes que temos, dessa forma:
package calculadora
func Adicione(x, y int) int {
return 7
}
O que Kent Beck, criador do método, fala sobre essa etapa é que devemos cometer quaisquer “pecados” (sim, o autor fala em sins no original em inglês) que nos façam obter o resultado esperado. Explorar os diferentes casos de teste e iterar o ciclo básico do TDD (escrever o teste, fazer o teste passar e refatorar para eliminar redundâncias) nos levará a superar quaisquer aberrações que adicionarmos nessa etapa.
Agora, quando rodarmos o teste novamente com o comando go test
, ele passará (aqui usamos a flag -v
, para exibir o resultado de forma verbosa, então o comando ficou assim: go test -v
).
=== RUN TestAdicione
--- PASS: TestAdicione (0.00s)
Passo 3: Refatorando para eliminar redundâncias
Nessa etapa, a ideia é eliminar todo tipo de duplicação que tenha sido inserida para os testes passarem. No nosso caso, onde está essa duplicação?
O número 7 aparece tanto no nosso teste (na linha 7 do arquivo calculadora_test.go
: espero := 7
) e no retorno da função sendo testada (na linha 4 do arquivo calculadora.go
: return 7
).
Como removemos essa duplicação? Bem, o caso esperado do teste precisa permanecer, afinal é ele que nos diz o que esperamos obter da função que está sendo testada. Assim, o que podemos mudar é o retorno da função: não mais fazê-la retornar o número 7 fixo, mas sim a soma de seus dois parâmetros.
package calculadora
func Adicione(x, y int) int {
return x + y
}
Quando rodamos os testes, eles permanecem passando:
=== RUN TestAdicione
--- PASS: TestAdicione (0.00s)
Com essa mudança, nosso código passa no caso de teste e está livre de redundâncias. Note que do passo 2 para o passo 3 os testes não devem quebrar: eles precisam continuar passando. Devemos remover as redundâncias e “pecados” do nosso código, mas não alterar a saída da função testada.
Em teoria, temos o código pronto e funcionando para outras somas que fizermos. Vamos testar se isso é verdade? Faremos isso adicionando um novo caso de testes ao nosso arquivo calculadora_test.go
:
package calculadora
import "testing"
func TestAdicione(t *testing.T) {
t.Run("a soma de 3 e 4 é igual a 7", func(t *testing.T) {
obtive := Adicione(3, 4)
espero := 7
if obtive != espero {
t.Errorf("espero '%d', mas obtive '%d'", espero, obtive)
}
})
t.Run("a soma de 27 e 113 é igual a 140", func(t *testing.T) {
obtive := Adicione(27, 113)
espero := 140
if obtive != espero {
t.Errorf("espero '%d', mas obtive '%d'", espero, obtive)
}
})
}
Note que adicionamos o método t.Run()
da biblioteca de testes (pacote testing
): ele roda sub-testes dentro de uma mesma função de testes, cada um desses sub-testes terá o nome que for passado como primeiro argumento. No exemplo, usamos os nomes "a soma de 3 e 4 é igual a 7" e "a soma de 27 e 113 é igual a 140".
Como esperávamos, os testes continuam passando! Esse é o resultado que obtemos ao rodar go test -v
:
=== RUN TestAdicione
=== RUN TestAdicione/a_soma_de_3_e_4_é_igual_a_7
=== RUN TestAdicione/a_soma_de_27_e_113_é_igual_a_140
--- PASS: TestAdicione (0.00s)
--- PASS: TestAdicione/a_soma_de_3_e_4_é_igual_a_7 (0.00s)
--- PASS: TestAdicione/a_soma_de_27_e_113_é_igual_a_140 (0.00s)
PASS
Assim, podemos seguir adicionando casos até alcançarmos todos os cenários que julgarmos necessários.
Testes orientados por tabela
Quando adicionamos o segundo caso de testes, pudemos notar bastante código sendo repetido no arquivo calculadora_test.go
. Quando os casos de teste são muito similares e nos levam a escrever código repetido, podemos aplicar os testes orientados por tabela (traduzido do inglês Table-driven tests).
Como faremos? Vamos por partes:
Primeiro, utilizaremos a sintaxe de struct
literal para representar os atributos que precisamos e escrever uma lista de testes para remover a duplicação de código dos próprios testes.
testes := []struct {
nome string
operando1 int
operando2 int
esperado int
}{
{
nome: "a soma de 3 e 4 é igual a 7",
operando1: 3,
operando2: 4,
esperado: 7,
},
{
nome: "a soma de 27 e 113 é igual a 140",
operando1: 27,
operando2: 113,
esperado: 140,
},
}
Tudo que precisamos de entrada para os testes está representado nessa struct: o nome do sub-teste, os dois operandos da soma e o resultado esperado.
Em seguida, vamos adicionar uma instrução for range
para percorrer todos os elementos da struct
"testes" e, em cada iteração:
Chamar o método que roda sub-testes, passando o nome de cada sub-teste
Chamar a função
Adicione()
, passando como argumentos os operandos 1 e 2Fazer a comparação entre esperado e obtido
Dessa forma:
for _, teste := range testes {
t.Run(teste.nome, func(t *testing.T) {
obtive := Adicione(teste.operando1, teste.operando2)
if obtive != teste.esperado {
t.Errorf("espero '%d', mas obtive '%d'", teste.esperado, obtive)
}
})
}
É importante aqui entendermos se tudo continua funcionando como o esperado. Desta forma, adicionamos todos os testes que nosso negócio planeja implementar com facilidade. Por exemplo, vamos adicionar mais dois casos de teste:
package calculadora
import "testing"
func TestAdicione(t *testing.T) {
testes := []struct {
nome string
operando1 int
operando2 int
esperado int
}{
{
nome: "a soma de 3 e 4 é igual a 7",
operando1: 3,
operando2: 4,
esperado: 7,
},
{
nome: "a soma de 27 e 113 é igual a 140",
operando1: 27,
operando2: 113,
esperado: 140,
},
{
nome: "a soma de 1 e -1 é igual a 0",
operando1: 1,
operando2: -1,
esperado: 0,
},
{
nome: "a soma de 15 e 1059 é igual a 1074",
operando1: 15,
operando2: 1059,
esperado: 1074,
},
}
for _, teste := range testes {
t.Run(teste.nome, func(t *testing.T) {
obtive := Adicione(teste.operando1, teste.operando2)
if obtive != teste.esperado {
t.Errorf("espero '%d', mas obtive '%d'", teste.esperado, obtive)
}
})
}
}
Se rodarmos o comando go test -v
, receberemos o seguinte:
=== RUN TestAdicione
=== RUN TestAdicione/a_soma_de_3_e_4_é_igual_a_7
=== RUN TestAdicione/a_soma_de_27_e_113_é_igual_a_140
=== RUN TestAdicione/a_soma_de_1_e_-1_é_igual_a_0
=== RUN TestAdicione/a_soma_de_15_e_1059_é_igual_a_1074
--- PASS: TestAdicione (0.00s)
--- PASS: TestAdicione/a_soma_de_3_e_4_é_igual_a_7 (0.00s)
--- PASS: TestAdicione/a_soma_de_27_e_113_é_igual_a_140 (0.00s)
--- PASS: TestAdicione/a_soma_de_1_e_-1_é_igual_a_0 (0.00s)
--- PASS: TestAdicione/a_soma_de_15_e_1059_é_igual_a_1074 (0.00s)
PASS
Percebeu que adicionar novos testes ficou muito simples?
Esse tipo de abordagem é bastante útil quando não existem (ou existem poucas) variações entre os casos de teste.
Conteúdo adicional
Aprenda Go com Testes
Sugerimos esse conteúdo para aprofundar os conhecimentos em testes (e em Go também): Aprenda Go com Testes
Documentação do pacote testing
da biblioteca padrão:
testing
da biblioteca padrão:Se inglês não for um problema, consulte aqui também:
Last updated