Menu

Testes em Golang: testes unitários, testes em tabela e benchmarks

Como testar código Go com o pacote padrão testing e o go test: arquivos _test.go, funções TestXxx, t.Errorf ou t.Fatalf, testes orientados a tabela com t.Run, helpers e diretórios temporários, cobertura, benchmarks com b.Loop e testes de exemplo.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

O código sob teste

As ferramentas de teste do Go fazem parte da toolchain padrão: o pacote testing e o comando go test. Nenhum framework para instalar, nenhuma biblioteca de asserções obrigatória.

Os exemplos desta página testam uma pequena função, Slugify, que transforma um título em um slug de URL. Aqui está ela, executável, com uma verificação feita à mão no main:

O editor do navegador executa o main; ele não consegue executar go test. Tudo abaixo está escrito como fica em um projeto real, com a saída do terminal mostrada depois de cada comando. Tudo foi executado com o Go 1.24.

O primeiro teste

Em um projeto, a função fica em um pacote, e os testes dela ficam ao lado, em um arquivo cujo nome termina em _test.go:

textutil/
├── go.mod          module example.com/textutil
├── slug.go         package textutil, func Slugify
└── slug_test.go    package textutil, the tests
// slug_test.go
package textutil

import "testing"

func TestSlugify(t *testing.T) {
	got := Slugify("Hello, World!")
	want := "hello-world"
	if got != want {
		t.Errorf("Slugify(%q) = %q, want %q", "Hello, World!", got, want)
	}
}

As regras que o go test usa:

  • Só arquivos terminados em _test.go são arquivos de teste. O go build os ignora, então o código de teste nunca vai parar no seu binário.
  • Um teste é uma função chamada TestXxx (a parte depois de Test não pode começar com letra minúscula) que recebe um *testing.T.
  • Um teste passa, a menos que chame um dos métodos de falha ou cause panic.
go test          # the package in the current directory
go test ./...    # every package in the module
$ go test
PASS
ok  	example.com/textutil	0.318s

O formato de mensagem de falha Func(entrada) = obtido, want esperado é a convenção do Go. Coloque a entrada na mensagem: uma falha que diz só got "a", want "b" manda você de volta ao código para descobrir qual era a entrada.

t.Errorf ou t.Fatalf

MétodoMarca como falhoPara o teste
t.Error, t.Errorfsimnão, continua executando
t.Fatal, t.Fatalfsimsim, na hora
t.Log, t.Logfnãonão, só imprime com -v ou em caso de falha
t.Skip, t.Skipfnãosim, reportado como pulado

Use Errorf por padrão, para que uma execução reporte todos os campos errados. Use Fatalf quando o resto do teste não puder funcionar, normalmente depois de um erro inesperado:

func TestCountWords(t *testing.T) {
	path := writeFile(t, "doc.txt", "the quick brown\nfox  jumps\n")

	got, err := CountWords(path)
	if err != nil {
		t.Fatalf("CountWords: unexpected error: %v", err) // stop: got is meaningless
	}
	if got != 5 {
		t.Errorf("CountWords = %d, want 5", got)
	}
}

O Fatal para o teste chamando runtime.Goexit, então ele precisa ser chamado na goroutine do próprio teste. A partir de uma goroutine que você iniciou, reporte com t.Error e retorne.

Testes orientados a tabela com t.Run

A maioria dos testes Go são tabelas: um slice de casos, um laço e t.Run para dar a cada caso o seu próprio nome.

func TestSlugifyTable(t *testing.T) {
	tests := []struct {
		name, in, want string
	}{
		{"simple", "Go Testing", "go-testing"},
		{"punctuation", "What's new in Go 1.24?", "what-s-new-in-go-1-24"},
		{"extra spaces", "  lots   of   space  ", "lots-of-space"},
		{"non-ascii dropped", "Café au lait", "caf-au-lait"},
		{"empty", "", ""},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := Slugify(tt.in); got != tt.want {
				t.Errorf("Slugify(%q) = %q, want %q", tt.in, got, tt.want)
			}
		})
	}
}

Acrescentar um caso é uma linha. Cada subteste é reportado separadamente, um Fatal dentro de um encerra só aquele subteste, e você pode executar um só pelo nome. Execute tudo com -v:

$ go test -v
=== RUN   TestSlugify
--- PASS: TestSlugify (0.00s)
=== RUN   TestSlugifyTable
=== RUN   TestSlugifyTable/simple
=== RUN   TestSlugifyTable/punctuation
=== RUN   TestSlugifyTable/extra_spaces
=== RUN   TestSlugifyTable/non-ascii_dropped
=== RUN   TestSlugifyTable/empty
--- PASS: TestSlugifyTable (0.00s)
    --- PASS: TestSlugifyTable/simple (0.00s)
    --- PASS: TestSlugifyTable/punctuation (0.00s)
    --- PASS: TestSlugifyTable/extra_spaces (0.00s)
    --- PASS: TestSlugifyTable/non-ascii_dropped (0.00s)
    --- PASS: TestSlugifyTable/empty (0.00s)
=== RUN   TestCountWords
--- PASS: TestCountWords (0.00s)
=== RUN   TestCountWordsMissingFile
--- PASS: TestCountWordsMissingFile (0.00s)
=== RUN   ExampleSlugify
--- PASS: ExampleSlugify (0.00s)
PASS
ok  	example.com/textutil	0.182s

Espaços nos nomes de subtestes viram underscores. Quando um caso falha, a saída nomeia o subteste e a linha:

--- FAIL: TestSlugifyTable (0.00s)
    --- FAIL: TestSlugifyTable/underscore (0.00s)
        slug_test.go:27: Slugify("snake_case_name") = "snake-case-name", want "snake_case_name"
FAIL
FAIL	example.com/textutil	0.289s

Desde o Go 1.22 cada iteração do laço tem o seu próprio tt, então closures dentro de t.Run veem o caso certo mesmo quando os subtestes executam em paralelo. Código mais antigo muitas vezes tem tt := tt no topo do corpo do laço por esse motivo; isso não é mais necessário.

Para executar subtestes em paralelo, chame t.Parallel() no início da função do subteste. Só faça isso quando os casos forem independentes e lentos o bastante para fazer diferença.

Flags do go test que você vai usar

ComandoFaz
go test -vimprime o nome e o resultado de cada teste e a saída de t.Log
go test -run TestSlugifyexecuta os testes cujos nomes casam com a expressão regular
go test -run 'TestSlugifyTable/empty'executa um subteste
go test -count=1ignora resultados em cache e executa de novo
go test -raceexecuta com o detector de data race
go test -shortavisa os testes longos para se pularem (if testing.Short() { t.Skip() })
go test -failfastpara depois do primeiro teste que falhar
go test -timeout 30sfalha se a execução levar mais tempo (padrão: 10 minutos)
go test -coverimprime a cobertura de instruções
go test -bench=.também executa os benchmarks

Quando você nomeia pacotes (go test ., go test ./...), o go test guarda em cache os resultados dos pacotes cujo código e cujas entradas não mudaram, e imprime (cached) depois deles. Um go test simples, sem argumentos, nunca usa cache. O cache acompanha os arquivos e as variáveis de ambiente que um teste lê pelo pacote os, mas não estado externo como um banco de dados ou um serviço de rede, então um teste que depende de um deles pode passar pelo cache quando falharia agora; -count=1 força uma execução real.

Helpers, diretórios temporários e limpeza

A chamada writeFile em TestCountWords, acima, é um helper de teste:

// writeFile is a test helper: t.Helper makes failures point at the caller.
func writeFile(t *testing.T, name, content string) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), name) // removed automatically after the test
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("writing %s: %v", name, err)
	}
	return path
}
  • t.Helper() marca a função como helper, então uma falha é reportada na linha do teste que a chamou, e não dentro do helper.
  • t.TempDir() cria um diretório novo e o apaga quando o teste termina. Os testes nunca precisam limpar arquivos por conta própria.
  • t.Cleanup(func() { ... }) registra qualquer outra desmontagem (fechar um servidor, apagar uma tabela). As limpezas executam depois do teste e dos seus subtestes, a última registrada primeiro.
  • t.Setenv("KEY", "value") define uma variável de ambiente só para este teste e a restaura depois.
  • t.Context() (Go 1.24) devolve um context que é cancelado logo antes das limpezas executarem.

Arquivos de fixture de teste ficam em um diretório chamado testdata ao lado dos testes. A ferramenta go não o trata como pacote, e os testes executam com o diretório do pacote como diretório de trabalho, então os.ReadFile("testdata/input.json") funciona.

Testando erros e handlers HTTP

Verifique erros com errors.Is ou errors.As, do mesmo jeito que o código que chama faria:

func TestCountWordsMissingFile(t *testing.T) {
	_, err := CountWords(filepath.Join(t.TempDir(), "nope.txt"))
	if !errors.Is(err, fs.ErrNotExist) {
		t.Errorf("err = %v, want fs.ErrNotExist", err)
	}
}

Comparar strings de erro quebra assim que alguém reescreve uma mensagem.

Para handlers HTTP, o net/http/httptest oferece um ResponseWriter falso, então você pode chamar o handler diretamente, sem rede:

func TestHealth(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/health", nil)
	rec := httptest.NewRecorder()

	healthHandler(rec, req)

	if rec.Code != http.StatusOK {
		t.Errorf("status = %d, want %d", rec.Code, http.StatusOK)
	}
	if body := rec.Body.String(); body != "ok\n" {
		t.Errorf("body = %q, want %q", body, "ok\n")
	}
}

httptest.NewServer inicia um servidor real em localhost para testar código cliente. A página de cliente HTTP o usa em todos os exemplos.

Cobertura

$ go test -cover
PASS
coverage: 100.0% of statements
ok  	example.com/textutil	0.578s

$ go test -coverprofile=cover.out
$ go tool cover -func=cover.out
example.com/textutil/slug.go:11:	Slugify		100.0%
example.com/textutil/words.go:9:	CountWords	100.0%
total:					(statements)	100.0%

$ go tool cover -html=cover.out    # opens a browser with covered lines in green

A cobertura diz quais linhas nunca executaram, o que é útil para encontrar ramos sem teste. Um número alto não diz se as asserções são boas: um teste que chama todas as funções e não verifica nada chega a 100%.

Benchmarks

Um benchmark é func BenchmarkXxx(b *testing.B). O Go 1.24 trouxe o b.Loop, que agora é a forma recomendada:

func BenchmarkSlugify(b *testing.B) {
	for b.Loop() {
		Slugify("The Go Programming Language, 2nd Edition")
	}
}

O b.Loop executa o corpo quantas vezes forem necessárias para uma medição estável, exclui da medição o código de preparação antes do laço e impede que o compilador elimine a chamada por otimização. Código escrito antes do Go 1.24 usa for i := 0; i < b.N; i++, que ainda funciona, mas precisa de b.ResetTimer() depois de uma preparação cara e pode ser enganado pela eliminação de código morto.

Benchmarks não executam com um go test simples. Peça por eles, e pule os testes normais com -run='^$':

$ go test -bench=. -benchmem -run='^$'
goos: darwin
goarch: arm64
pkg: example.com/textutil
cpu: Apple M4
BenchmarkSlugify-10    	 5061945	       238.1 ns/op	     168 B/op	       5 allocs/op
PASS
ok  	example.com/textutil	1.410s

As colunas são: o nome com o GOMAXPROCS acrescentado, as iterações executadas, o tempo por chamada, os bytes alocados por chamada e as alocações por chamada. Os números dependem da máquina; compare execuções na mesma máquina. Para comparar de forma confiável antes e depois de uma mudança, execute cada lado várias vezes com -count=10 e passe as duas saídas para o benchstat (golang.org/x/perf/cmd/benchstat).

Exemplos também são testes

Uma função de exemplo imprime algo e declara a saída esperada em um comentário. O go test a executa e falha se a saída for diferente, e o go doc e o pkg.go.dev a mostram como documentação:

// example_test.go
package textutil_test

import (
	"fmt"

	"example.com/textutil"
)

func ExampleSlugify() {
	fmt.Println(textutil.Slugify("Hello, World!"))
	// Output: hello-world
}

O nome de pacote textutil_test torna isto um teste externo: ele só pode usar a API exportada, como um código de verdade que chama faria. Esses arquivos podem ficar no mesmo diretório do pacote. Sem um comentário // Output:, o exemplo é compilado, mas não executado. Use // Unordered output: quando as linhas podem aparecer em qualquer ordem.

Fuzzing, em poucas palavras

O Go 1.18 trouxe os testes de fuzzing, que geram entradas para encontrar crashes e invariantes quebradas:

func FuzzSlugify(f *testing.F) {
	f.Add("Hello, World!") // seed input
	f.Fuzz(func(t *testing.T, s string) {
		slug := Slugify(s)
		if strings.Contains(slug, "--") || strings.HasPrefix(slug, "-") {
			t.Errorf("Slugify(%q) = %q: bad hyphens", s, slug)
		}
	})
}

Um go test simples executa só as entradas semente (os valores de f.Add e os arquivos salvos em testdata/fuzz). go test -fuzz=FuzzSlugify continua gerando entradas novas até encontrar uma falha ou você parar, e salva as entradas que falham em testdata/fuzz para que virem casos de teste normais.

Erros comuns

  • Arquivo de teste no lugar errado ou com nome errado. Ele precisa terminar em _test.go e ficar no diretório do pacote. slug_tests.go é compilado dentro do pacote, e não executado como teste.
  • Minúscula depois de Test. Testslugify não é um teste. TestSlugify e Test_slugify são.
  • t.Fatal a partir de outra goroutine. Reporte com t.Error ali e espere a goroutine antes de o teste retornar.
  • Mensagens sem a entrada. Inclua o que foi passado, o que saiu e o que era esperado.
  • Confiar em um resultado do cache. Use -count=1 quando um teste depende de algo fora do pacote.
  • Testes que dependem da ordem uns dos outros ou de variáveis globais compartilhadas. Cada teste deve preparar o próprio estado, e é para isso que existem t.TempDir, t.Setenv e t.Cleanup.

Perguntas frequentes

Como escrever um teste unitário em Go?

Crie um arquivo terminado em _test.go no mesmo diretório do código, importe testing e escreva uma função func TestNome(t *testing.T) que chama o seu código e reporta as divergências com t.Errorf. Execute go test nesse diretório, ou go test ./... para o módulo inteiro. Nenhum framework ou biblioteca de asserções é necessário.

Qual a diferença entre t.Error e t.Fatal em Go?

t.Error e t.Errorf marcam o teste como falho e continuam a executá-lo, então uma execução pode reportar vários problemas. t.Fatal e t.Fatalf marcam como falho e param o teste na hora. Use Fatal quando continuar não faz sentido, como depois de um erro inesperado que deixa o resultado inutilizável.

Como executar um único teste em Go?

Passe uma expressão regular para -run: go test -run TestSlugify executa todo teste cujo nome casa, e go test -run 'TestSlugifyTable/punctuation' executa um subteste (espaços nos nomes de subtestes viram underscores). Acrescente -v para ver o nome e o resultado de cada teste, e -count=1 para ignorar o cache de testes.

Como escrever um benchmark em Go?

Escreva func BenchmarkNome(b *testing.B) em um arquivo _test.go e coloque o código a medir em um laço for b.Loop() { ... } (Go 1.24; código mais antigo usa for i := 0; i < b.N; i++). Execute com go test -bench=. -benchmem, que informa nanossegundos, bytes e alocações por operação.

Como ver a cobertura de testes em Go?

go test -cover imprime a porcentagem de instruções que os testes executaram. Para detalhes, grave um perfil com go test -coverprofile=cover.out e depois execute go tool cover -func=cover.out para números por função ou go tool cover -html=cover.out para ver as linhas cobertas e não cobertas no navegador.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR