O que são testes em Go e como o pacote testing funciona
Em Go, testes automatizados são escritos em arquivos com sufixo _test.go e executados com go test. O pacote padrão testing fornece o tipo *testing.T, que você usa para sinalizar falhas, registrar mensagens e controlar subtestes.
- Arquivos:
algo_test.go(mesmo pacote do código testado). - Funções de teste: começam com
Teste recebemt *testing.T. - Falha:
t.Error/t.Errorfmarca falha e continua;t.Fatal/t.Fatalfmarca falha e interrompe o teste atual. - Logs:
t.Log/t.Logfajudam a depurar (aparecem com-v).
Estrutura mínima de um teste
package util
import "testing"
func TestSoma(t *testing.T) {
got := Soma(2, 3)
want := 5
if got != want {
t.Fatalf("Soma(2,3) = %d; want %d", got, want)
}
}O padrão é sempre: preparar entradas, executar a função e comparar o resultado com o esperado.
Passo a passo: criando um arquivo *_test.go e rodando
1) Crie o arquivo de teste no mesmo diretório do pacote
Exemplo de estrutura (um pacote utilitário simples):
util/
math.go
math_test.go2) Escreva o teste com o mesmo package
Você pode testar no mesmo pacote (package util) para acessar itens não exportados, ou em pacote externo (package util_test) para testar apenas a API pública. Para iniciantes, comece com o mesmo pacote.
3) Execute
go test ./...O ./... executa testes em todos os pacotes do módulo.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
Comparação de resultados: boas práticas e armadilhas comuns
Comparando tipos simples
Para números, strings e bool, use comparação direta:
if got != want {
t.Errorf("got %v; want %v", got, want)
}Comparando structs e slices
Para structs comparáveis (sem slices, maps ou funcs), == funciona. Para slices, maps e structs que os contenham, use reflect.DeepEqual (padrão) ou compare campo a campo.
import (
"reflect"
"testing"
)
func TestTokens(t *testing.T) {
got := Tokens("a b")
want := []string{"a", "b"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("Tokens() = %#v; want %#v", got, want)
}
}Dica: ao imprimir slices/structs em falhas, %#v costuma ser mais informativo.
Comparando erros
Quando uma função retorna (T, error), teste os dois aspectos: se o erro era esperado e, quando não há erro, se o valor está correto.
func TestParseID(t *testing.T) {
got, err := ParseID("10")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != 10 {
t.Fatalf("got %d; want %d", got, 10)
}
}Para casos em que o erro é esperado:
func TestParseID_Invalid(t *testing.T) {
_, err := ParseID("x")
if err == nil {
t.Fatalf("expected error, got nil")
}
}Se você usa erros sentinela ou wrapping no seu pacote, prefira errors.Is para checar o tipo/identidade do erro, e não a mensagem.
Subtests com t.Run: organizando cenários
t.Run permite dividir um teste em subtestes nomeados. Isso melhora a leitura e facilita rodar apenas um cenário específico.
func TestNormalize(t *testing.T) {
t.Run("trim spaces", func(t *testing.T) {
got := Normalize(" a ")
want := "a"
if got != want {
t.Fatalf("got %q; want %q", got, want)
}
})
t.Run("lowercase", func(t *testing.T) {
got := Normalize("Go")
want := "go"
if got != want {
t.Fatalf("got %q; want %q", got, want)
}
})
}Use nomes curtos e descritivos. Em falhas, o output mostrará o caminho do subteste, ajudando a localizar o cenário.
Table-driven tests (testes orientados a tabela)
Testes orientados a tabela são um padrão idiomático em Go: você define uma lista de casos (entradas e saídas esperadas) e itera sobre ela. Isso reduz repetição e incentiva cobertura de bordas.
Estrutura típica
func TestSoma_Table(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{name: "positivos", a: 2, b: 3, want: 5},
{name: "zero", a: 0, b: 7, want: 7},
{name: "negativos", a: -2, b: -3, want: -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Soma(tt.a, tt.b)
if got != tt.want {
t.Fatalf("Soma(%d,%d) = %d; want %d", tt.a, tt.b, got, tt.want)
}
})
}
}Atenção ao loop variable capture
Em versões modernas do Go, o comportamento do range foi ajustado para reduzir armadilhas, mas ainda é uma boa prática “fixar” a variável do caso dentro do loop quando você usa closures, especialmente se o projeto pode compilar com versões diferentes:
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
// usa tt com segurança
})
}Table-driven com erro esperado
func TestParseID_Table(t *testing.T) {
tests := []struct {
name string
in string
want int
wantErr bool
}{
{name: "ok", in: "42", want: 42, wantErr: false},
{name: "empty", in: "", want: 0, wantErr: true},
{name: "invalid", in: "x", want: 0, wantErr: true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := ParseID(tt.in)
if (err != nil) != tt.wantErr {
t.Fatalf("ParseID(%q) err=%v; wantErr=%v", tt.in, err, tt.wantErr)
}
if err == nil && got != tt.want {
t.Fatalf("ParseID(%q)=%d; want %d", tt.in, got, tt.want)
}
})
}
}Flags úteis do go test no dia a dia
Rodar um teste específico com -run
-run filtra por regex no nome do teste/subteste.
go test ./util -run TestParseID
go test ./util -run TestParseID_Table/invalidRepetir execução com -count
Por padrão, o Go pode reutilizar resultados (cache) quando nada mudou. Para forçar reexecução:
go test ./... -count=1Isso é útil quando você está depurando testes ou quando há dependência de tempo/aleatoriedade (idealmente, evite testes flakey).
Cobertura com -cover e variações
go test ./... -coverPara gerar um perfil de cobertura e inspecionar depois:
go test ./... -coverprofile=cover.out
go tool cover -func=cover.out-func mostra cobertura por função, ajudando a identificar pontos sem teste.
Exercício prático: testar um pacote utilitário
Suponha que você já tenha um pacote utilitário com funções de validação e conversão. Abaixo está um exemplo de API típica para você adaptar ao seu pacote (use os nomes reais do que você criou anteriormente).
1) Código do pacote (exemplo para contextualizar o exercício)
// arquivo: util/strings.go
package util
import (
"errors"
"strings"
)
var ErrEmpty = errors.New("empty input")
func Slug(s string) (string, error) {
s = strings.TrimSpace(s)
if s == "" {
return "", ErrEmpty
}
s = strings.ToLower(s)
s = strings.Join(strings.Fields(s), "-")
return s, nil
}Agora crie testes cobrindo: entrada normal, múltiplos espaços, letras maiúsculas e erro para string vazia.
2) Crie util/strings_test.go com table-driven tests e subtests
package util
import (
"errors"
"testing"
)
func TestSlug(t *testing.T) {
tests := []struct {
name string
in string
want string
wantErr error
}{
{name: "simple", in: "Go Lang", want: "go-lang", wantErr: nil},
{name: "trim", in: " Hello ", want: "hello", wantErr: nil},
{name: "many spaces", in: "a b c", want: "a-b-c", wantErr: nil},
{name: "empty", in: " ", want: "", wantErr: ErrEmpty},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
got, err := Slug(tt.in)
if tt.wantErr != nil {
if err == nil {
t.Fatalf("expected error %v, got nil", tt.wantErr)
}
if !errors.Is(err, tt.wantErr) {
t.Fatalf("err=%v; want %v", err, tt.wantErr)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tt.want {
t.Fatalf("Slug(%q)=%q; want %q", tt.in, got, tt.want)
}
})
}
}3) Rode apenas esse teste e confira cobertura
go test ./util -run TestSlug -count=1 -v
go test ./util -coverComo extensão do exercício, adicione casos de borda relevantes ao seu pacote: strings com tabs/linhas, caracteres especiais, entradas muito longas, etc. Se sua função tiver regras específicas, transforme cada regra em um caso na tabela.
Padrões de organização de testes e fixtures simples
Onde colocar testes
- Ao lado do código:
arquivo.goearquivo_test.gono mesmo diretório do pacote (padrão mais comum). - Pacote externo: use
package nome_testquando quiser testar apenas a API pública e evitar acoplamento a detalhes internos.
Nomeando testes e subtestes
TestFuncaopara o teste principal.- Subtestes com nomes curtos:
"ok","empty","invalid","edge-case". - Em table-driven tests, prefira um campo
namee uset.Run(tt.name, ...).
Arrange-Act-Assert (AAA) no estilo Go
Mesmo sem framework, mantenha a disciplina:
- Arrange: declare entradas e expectativas (
tests := ...). - Act: chame a função.
- Assert: compare e falhe com mensagens úteis.
Helpers de teste para reduzir repetição
Quando você repete validações, crie funções auxiliares no próprio arquivo de teste. Exemplo: um helper para checar erro esperado.
func requireErrIs(t *testing.T, err error, want error) {
t.Helper()
if err == nil {
t.Fatalf("expected error %v, got nil", want)
}
// errors.Is exige import "errors"
if !errors.Is(err, want) {
t.Fatalf("err=%v; want %v", err, want)
}
}t.Helper() faz com que a linha reportada em falhas aponte para o teste chamador, e não para o helper.
Fixtures simples (sem complicar)
Fixture é um conjunto de dados/estado usado por testes. Em Go, comece simples:
- Constantes e variáveis no topo do arquivo de teste para entradas comuns.
- Funções builder para montar structs de forma consistente.
- Arquivos de teste em
testdata/quando precisar ler conteúdo do disco. O diretóriotestdataé ignorado por ferramentas como pacote importável, e é um padrão para dados de teste.
// Exemplo de builder simples
func newUserForTest() User {
return User{Name: "Ana", Email: "ana@example.com"}
}Evite fixtures gigantes: prefira dados mínimos que exercitem o comportamento desejado. Quando um teste falha, quanto menor o cenário, mais rápido você entende o motivo.