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.gosão arquivos de teste. Ogo buildos 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 deTestnã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étodo | Marca como falho | Para o teste |
|---|---|---|
t.Error, t.Errorf | sim | não, continua executando |
t.Fatal, t.Fatalf | sim | sim, na hora |
t.Log, t.Logf | não | não, só imprime com -v ou em caso de falha |
t.Skip, t.Skipf | não | sim, 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
| Comando | Faz |
|---|---|
go test -v | imprime o nome e o resultado de cada teste e a saída de t.Log |
go test -run TestSlugify | executa os testes cujos nomes casam com a expressão regular |
go test -run 'TestSlugifyTable/empty' | executa um subteste |
go test -count=1 | ignora resultados em cache e executa de novo |
go test -race | executa com o detector de data race |
go test -short | avisa os testes longos para se pularem (if testing.Short() { t.Skip() }) |
go test -failfast | para depois do primeiro teste que falhar |
go test -timeout 30s | falha se a execução levar mais tempo (padrão: 10 minutos) |
go test -cover | imprime 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.goe ficar no diretório do pacote.slug_tests.goé compilado dentro do pacote, e não executado como teste. - Minúscula depois de
Test.Testslugifynão é um teste.TestSlugifyeTest_slugifysão. t.Fatala partir de outra goroutine. Reporte comt.Errorali 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=1quando 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.Setenvet.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.