Testes básicos em Go: pacote testing, table-driven tests e cobertura

Capítulo 11

Tempo estimado de leitura: 8 minutos

+ Exercício

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 Test e recebem t *testing.T.
  • Falha: t.Error/t.Errorf marca falha e continua; t.Fatal/t.Fatalf marca falha e interrompe o teste atual.
  • Logs: t.Log/t.Logf ajudam 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.go

2) 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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/invalid

Repetir execução com -count

Por padrão, o Go pode reutilizar resultados (cache) quando nada mudou. Para forçar reexecução:

go test ./... -count=1

Isso é ú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 ./... -cover

Para 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 -cover

Como 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.go e arquivo_test.go no mesmo diretório do pacote (padrão mais comum).
  • Pacote externo: use package nome_test quando quiser testar apenas a API pública e evitar acoplamento a detalhes internos.

Nomeando testes e subtestes

  • TestFuncao para o teste principal.
  • Subtestes com nomes curtos: "ok", "empty", "invalid", "edge-case".
  • Em table-driven tests, prefira um campo name e use t.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ório testdata é 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.

Agora responda o exercício sobre o conteúdo:

Em um teste table-driven que usa t.Run com uma função anônima, qual prática ajuda a evitar problemas ao usar a variável do loop dentro do subteste?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Ao usar closures em subtestes, é comum “fixar” a variável do loop (ex.: tt := tt) antes do t.Run. Isso evita que a função anônima capture a variável do range de forma inesperada em diferentes versões do Go.

Próximo capitúlo

Entrada e saída em Go: arquivos, diretórios e leitura eficiente

Arrow Right Icon
Capa do Ebook gratuito Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos
61%

Go (Golang) para Iniciantes: Fundamentos, Concorrência e Estrutura de Projetos

Novo curso

18 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.