---
title: "Testes de Tabela em Go: Guia Definitivo"
url: "https://golang.com.br/blog/testes-tabela-go-guia-table-driven-tests/"
markdown_url: "https://golang.com.br/blog/testes-tabela-go-guia-table-driven-tests.MD"
description: "Aprenda table-driven tests em Go: crie testes de tabela idiomáticos com t.Run(), subtests paralelos, edge cases e exemplos prontos para copiar no seu projeto."
date: "2026-05-12"
author: "Golang Brasil"
---

# Testes de Tabela em Go: Guia Definitivo

Aprenda table-driven tests em Go: crie testes de tabela idiomáticos com t.Run(), subtests paralelos, edge cases e exemplos prontos para copiar no seu projeto.


Table-driven tests são **o padrão idiomático de testes em Go**. Se você já leu o código fonte da standard library, percebeu que praticamente todo teste segue esse formato: um slice de structs com casos de teste, iterado com `t.Run()`. Não é por acaso — essa abordagem é concisa, extensível e fácil de manter.

Neste guia, você vai dominar table-driven tests desde a estrutura básica até subtests paralelos, passando por exemplos reais com handlers HTTP, validadores e parsers.

## Por que Table-Driven Tests São o Padrão em Go

Testes tradicionais repetem muito código. Para cada cenário, você escreve uma função ou bloco separado com setup, execução e asserção. Com table-driven tests, **os dados ficam separados da lógica de teste**:

- **Menos duplicação**: a lógica de asserção é escrita uma vez
- **Fácil de estender**: adicionar um caso é adicionar uma linha na tabela
- **Legibilidade**: cada caso documenta o cenário no próprio nome
- **Subtests nativos**: `t.Run()` gera subtests nomeados no output

Se você está começando com testes em Go, veja primeiro o [guia de testes](/aprenda/testes-go/) para entender o básico do pacote `testing`. Table-driven tests são o próximo passo natural.

## Estrutura Básica

A estrutura clássica usa um slice anônimo de [structs](/glossario/struct/) com campos para input, output esperado e nome do caso:

```go
package mathutil

import "testing"

func TestSoma(t *testing.T) {
	tests := []struct {
		name     string
		a, b     int
		expected int
	}{
		{name: "positivos", a: 2, b: 3, expected: 5},
		{name: "zero", a: 0, b: 0, expected: 0},
		{name: "negativos", a: -1, b: -2, expected: -3},
		{name: "misto", a: -5, b: 10, expected: 5},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := Soma(tt.a, tt.b)
			if got != tt.expected {
				t.Errorf("Soma(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
			}
		})
	}
}
```

Execute e veja cada caso como um subtest nomeado:

```bash
go test -v -run TestSoma
```

Saída:

```
=== RUN   TestSoma
=== RUN   TestSoma/positivos
=== RUN   TestSoma/zero
=== RUN   TestSoma/negativos
=== RUN   TestSoma/misto
--- PASS: TestSoma (0.00s)
```

A variável `tt` (abreviação de "table test") é convenção na comunidade Go. O campo `name` é passado para `t.Run()`, criando subtests que podem ser executados individualmente com `-run TestSoma/negativos`.

## Testando Erros e Edge Cases

Em funções que retornam [error](/glossario/error/), adicione um campo `wantErr` à tabela:

```go
package parser

import (
	"errors"
	"testing"
)

func TestParsePort(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		want    int
		wantErr bool
	}{
		{name: "porta válida", input: "8080", want: 8080, wantErr: false},
		{name: "porta mínima", input: "1", want: 1, wantErr: false},
		{name: "porta máxima", input: "65535", want: 65535, wantErr: false},
		{name: "zero inválido", input: "0", want: 0, wantErr: true},
		{name: "negativo", input: "-1", want: 0, wantErr: true},
		{name: "acima do limite", input: "65536", want: 0, wantErr: true},
		{name: "não numérico", input: "abc", want: 0, wantErr: true},
		{name: "string vazia", input: "", want: 0, wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ParsePort(tt.input)
			if (err != nil) != tt.wantErr {
				t.Fatalf("ParsePort(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
			}
			if got != tt.want {
				t.Errorf("ParsePort(%q) = %d, want %d", tt.input, got, tt.want)
			}
		})
	}
}
```

Essa abordagem de [tratamento de erros](/aprenda/golang-erros/) nos testes é idiomática: use `t.Fatalf` para erros que impedem a continuação e `t.Errorf` para falhas que ainda permitem verificar outros campos.

## Subtests Paralelos

Para acelerar suítes com muitos casos, execute subtests em paralelo com `t.Parallel()`. Atenção ao closure — capture a variável do loop:

```go
func TestValidateEmail(t *testing.T) {
	tests := []struct {
		name  string
		email string
		valid bool
	}{
		{name: "válido simples", email: "user@example.com", valid: true},
		{name: "com subdomínio", email: "user@mail.example.com", valid: true},
		{name: "sem arroba", email: "userexample.com", valid: false},
		{name: "sem domínio", email: "user@", valid: false},
		{name: "vazio", email: "", valid: false},
		{name: "com espaços", email: "user @example.com", valid: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			got := ValidateEmail(tt.email)
			if got != tt.valid {
				t.Errorf("ValidateEmail(%q) = %v, want %v", tt.email, got, tt.valid)
			}
		})
	}
}
```

Desde o Go 1.22, a variável de loop tem escopo por iteração, eliminando o antigo problema de captura de closure. Se você usa [Go 1.22+](/blog/go-1-22/), não precisa mais da linha `tt := tt` antes do `t.Run`.

## Testando Handlers HTTP

Table-driven tests brilham ao testar [APIs REST](/blog/como-criar-api-rest-go-net-http-sem-framework/). Use `httptest.NewRecorder()` para simular requisições:

```go
package api

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestHealthHandler(t *testing.T) {
	tests := []struct {
		name       string
		method     string
		wantStatus int
		wantBody   string
	}{
		{name: "GET retorna 200", method: http.MethodGet, wantStatus: http.StatusOK, wantBody: `{"status":"ok"}`},
		{name: "POST retorna 405", method: http.MethodPost, wantStatus: http.StatusMethodNotAllowed, wantBody: ""},
		{name: "PUT retorna 405", method: http.MethodPut, wantStatus: http.StatusMethodNotAllowed, wantBody: ""},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			req := httptest.NewRequest(tt.method, "/health", nil)
			rec := httptest.NewRecorder()

			HealthHandler(rec, req)

			if rec.Code != tt.wantStatus {
				t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
			}
			if tt.wantBody != "" && rec.Body.String() != tt.wantBody {
				t.Errorf("body = %q, want %q", rec.Body.String(), tt.wantBody)
			}
		})
	}
}
```

Para handlers que dependem de [context](/blog/context-go-como-usar-corretamente/), adicione um campo `ctx` à tabela ou use um helper que injeta valores no contexto de cada requisição.

## Tabelas com Funções de Comparação

Quando a comparação é complexa (structs com campos não exportados, slices, maps), use funções de comparação na própria tabela:

```go
func TestParseConfig(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		check   func(t *testing.T, cfg *Config, err error)
	}{
		{
			name:  "config válida",
			input: `{"port": 8080, "debug": true}`,
			check: func(t *testing.T, cfg *Config, err error) {
				t.Helper()
				if err != nil {
					t.Fatalf("erro inesperado: %v", err)
				}
				if cfg.Port != 8080 {
					t.Errorf("port = %d, want 8080", cfg.Port)
				}
			},
		},
		{
			name:  "JSON inválido",
			input: `{invalid}`,
			check: func(t *testing.T, cfg *Config, err error) {
				t.Helper()
				if err == nil {
					t.Fatal("esperava erro, got nil")
				}
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg, err := ParseConfig([]byte(tt.input))
			tt.check(t, cfg, err)
		})
	}
}
```

Note o uso de `t.Helper()` — ele faz com que, ao falhar, o Go reporte a linha de quem chamou a função de check, não a linha interna do helper. Esse padrão é essencial para manter as mensagens de erro úteis.

## Convenções e Boas Práticas

1. **Nomeie cada caso claramente** — o nome aparece no output do teste e no `-run` filter
2. **Comece pelo cenário feliz** — facilita a leitura ao revisar a tabela
3. **Teste edge cases explicitamente** — string vazia, zero, nil, valores limítrofes
4. **Use `t.Helper()` em funções auxiliares** — melhora a rastreabilidade de erros
5. **Prefira `t.Fatalf` para erros bloqueantes** — e `t.Errorf` para verificações independentes
6. **Mantenha a tabela ordenada** — agrupe por tipo de cenário (válidos, inválidos, edge cases)

Para projetos maiores, combine table-driven tests com [testes de integração via Testcontainers](/blog/go-testcontainers-testes-integracao-containers/) e [fuzzing nativo](/blog/fuzzing-go-testes-nativos/) para cobertura completa.

## Table-Driven Tests vs Fuzzing vs Benchmarks

| Técnica | Propósito | Quando usar |
|---------|-----------|-------------|
| **Table-driven tests** | Validar comportamento esperado | Cenários conhecidos, regressão |
| **[Fuzzing](/blog/fuzzing-go-testes-nativos/)** | Descobrir bugs desconhecidos | Parsers, validadores, segurança |
| **[Benchmarks](/glossario/benchmark/)** | Medir performance | Otimização, comparação de abordagens |

Table-driven tests são a base da pirâmide de testes em Go. Use [generics](/blog/generics-go-guia-pratico/) para criar helpers de teste reutilizáveis que funcionem com qualquer tipo, e [slog](/blog/slog-go-logging-estruturado/) para logging estruturado que facilite o debug quando um teste falha em CI.

## Exemplo Real: Validador de CPF

Um exemplo completo que combina tudo — edge cases, erros e cenários reais:

```go
func TestValidaCPF(t *testing.T) {
	tests := []struct {
		name    string
		cpf     string
		wantErr bool
	}{
		{name: "CPF válido formatado", cpf: "123.456.789-09", wantErr: false},
		{name: "CPF válido sem formato", cpf: "12345678909", wantErr: false},
		{name: "todos iguais", cpf: "111.111.111-11", wantErr: true},
		{name: "dígito errado", cpf: "123.456.789-00", wantErr: true},
		{name: "curto demais", cpf: "1234", wantErr: true},
		{name: "longo demais", cpf: "123456789012345", wantErr: true},
		{name: "vazio", cpf: "", wantErr: true},
		{name: "letras", cpf: "abc.def.ghi-jk", wantErr: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			err := ValidaCPF(tt.cpf)
			if (err != nil) != tt.wantErr {
				t.Errorf("ValidaCPF(%q) error = %v, wantErr %v", tt.cpf, err, tt.wantErr)
			}
		})
	}
}
```

Esse padrão se aplica a qualquer validação — emails, CEPs, telefones. A tabela é a documentação viva dos cenários aceitos e rejeitados.

## Organizando Testes em Projetos Maiores

Em projetos com [clean architecture](/tutoriais/go-clean-architecture/) ou múltiplos [packages](/glossario/package/), mantenha os testes no mesmo package (`_test.go` ao lado do código). Para testes que precisam de [interfaces](/glossario/interface/) mockadas, crie um pacote `testutil` com helpers compartilhados.

Se você usa [Go embed](/blog/go-embed-embutir-arquivos-binario/) para embutir fixtures de teste (arquivos JSON, SQL, etc.), combine com table-driven tests para iterar sobre múltiplos arquivos de input automaticamente.

## Próximos Passos

Table-driven tests são a fundação de código Go testável e mantível. Combine com [fuzzing](/blog/fuzzing-go-testes-nativos/) para encontrar bugs que você não imaginou, [Testcontainers](/blog/go-testcontainers-testes-integracao-containers/) para testes de integração com bancos reais, e [PGO](/blog/go-pgo-profile-guided-optimization-performance/) para otimizar o código que seus testes validam.

Quer ver como outras linguagens lidam com testes? Confira como <a href="https://rustlang.com.br/blog/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'rustlang.com.br' })">Rust</a> implementa testes integrados ao compilador com macros como `assert_eq!`, ou como <a href="https://python.dev.br/blog/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">Python</a> usa pytest para parametrização de testes — o equivalente de table-driven tests no ecossistema Python.

## FAQ

### O que são table-driven tests em Go?

Table-driven tests são um padrão idiomático onde você define casos de teste como um slice de structs e itera sobre eles com `t.Run()`. Cada struct contém inputs, outputs esperados e um nome descritivo. É o padrão mais usado na standard library do Go.

### Qual a diferença entre t.Errorf e t.Fatalf em testes de tabela?

`t.Errorf` registra a falha mas continua executando o teste, permitindo verificar outros campos. `t.Fatalf` para a execução do subtest imediatamente. Use `t.Fatalf` para erros que invalidam as verificações seguintes (como um erro inesperado) e `t.Errorf` para comparações independentes.

### Como rodar apenas um caso específico de um table-driven test?

Use a flag `-run` com o nome do teste e do subteste: `go test -run TestSoma/negativos`. O nome do subteste é o valor passado para `t.Run()`. Regex é suportado, então `go test -run TestSoma/` roda todos os subtests de TestSoma.

### Posso usar table-driven tests com t.Parallel()?

Sim. Chame `t.Parallel()` dentro do `t.Run()` para executar subtests em paralelo. Desde o Go 1.22, a variável de loop tem escopo por iteração, então não é mais necessário capturá-la com `tt := tt` antes do closure.

### Table-driven tests substituem fuzzing?

Não. Table-driven tests validam cenários conhecidos e documentados. Fuzzing descobre bugs com inputs aleatórios que você não imaginou. Use ambos: table-driven para regressão e comportamento esperado, fuzzing para robustez e segurança.
