Testes orientados a tabela paralelos em Go
Acelere os testes em Go com execução paralela
Testes orientados por tabela são a abordagem idiomática no Go para testar múltiplos cenários de forma eficiente.
Quando combinados com a execução paralela usando t.Parallel(), você pode reduzir drasticamente o tempo de execução do conjunto de testes, especialmente para operações limitadas por E/S.
No entanto, a execução paralela de testes introduz desafios únicos em torno de condições de corrida e isolamento de testes que exigem atenção cuidadosa.

Entendendo a execução paralela de testes
O pacote de testes do Go fornece suporte integrado à execução paralela de testes através do método t.Parallel(). Quando um teste chama t.Parallel(), ele sinaliza ao executor de testes que esse teste pode ser executado simultaneamente com outros testes paralelos. Isso é particularmente poderoso quando combinado com testes orientados por tabela, onde você tem muitos casos de teste independentes que podem ser executados simultaneamente.
A paralelismo padrão é controlado por GOMAXPROCS, que normalmente é igual ao número de núcleos de CPU no seu computador. Você pode ajustar isso com a bandeira -parallel: go test -parallel 4 limita os testes simultâneos a 4, independentemente da contagem de CPU. Isso é útil para controlar o uso de recursos ou quando os testes têm requisitos específicos de concorrência.
Para desenvolvedores novos no teste em Go, entender os fundamentos é crucial. Nosso guia sobre melhores práticas para testes unitários no Go aborda testes orientados por tabela, subtestes e os fundamentos do pacote de testes que formam a base para a execução paralela.
Padrão Básico de Testes Orientados por Tabela em Paralelo
Aqui está o padrão correto para testes orientados por tabela em paralelo:
func TestCalculate(t *testing.T) {
tests := []struct {
name string
a, b int
op string
expected int
wantErr bool
}{
{"adição", 2, 3, "+", 5, false},
{"subtração", 5, 3, "-", 2, false},
{"multiplicação", 4, 3, "*", 12, false},
{"divisão", 10, 2, "/", 5, false},
{"divisão por zero", 10, 0, "/", 0, true},
}
for _, tt := range tests {
tt := tt // Capturar variável de loop
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Habilitar execução paralela
result, err := Calculate(tt.a, tt.b, tt.op)
if (err != nil) != tt.wantErr {
t.Errorf("Calculate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if result != tt.expected {
t.Errorf("Calculate(%d, %d, %q) = %d; want %d",
tt.a, tt.b, tt.op, result, tt.expected)
}
})
}
}
A linha crítica é tt := tt antes de t.Run(). Isso captura o valor atual da variável de loop, garantindo que cada subteste paralelo opere com sua própria cópia dos dados do caso de teste.
O Problema da Captura da Variável de Loop
Este é um dos erros mais comuns ao usar t.Parallel() com testes orientados por tabela. No Go, a variável de loop tt é compartilhada entre todas as iterações. Quando os subtestes são executados em paralelo, eles podem todos se referir à mesma variável tt, que é sobrescrita conforme o loop continua. Isso leva a condições de corrida e falhas de teste imprevisíveis.
Incorreto (condição de corrida):
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Todos os subtestes podem ver o mesmo valor de tt!
result := Calculate(tt.a, tt.b, tt.op)
})
}
Correto (variável capturada):
for _, tt := range tests {
tt := tt // Capturar a variável de loop
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Cada subteste tem sua própria cópia de tt
result := Calculate(tt.a, tt.b, tt.op)
})
}
A atribuição tt := tt cria uma nova variável com escopo para a iteração do loop, garantindo que cada goroutine tenha sua própria cópia dos dados do caso de teste.
Garantindo a Independência dos Testes
Para que os testes em paralelo funcionem corretamente, cada teste deve ser completamente independente. Eles não devem:
- Compartilhar estado global ou variáveis
- Modificar recursos compartilhados sem sincronização
- Depender da ordem de execução
- Acessar os mesmos arquivos, bancos de dados ou recursos de rede sem coordenação
Exemplo de testes paralelos independentes:
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{"e-mail válido", "user@example.com", false},
{"formato inválido", "not-an-email", true},
{"domínio ausente", "user@", true},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v",
tt.email, err, tt.wantErr)
}
})
}
}
Cada caso de teste opera com seus próprios dados de entrada, sem estado compartilhado, tornando seguro para execução paralela.
Detectando Condições de Corrida
O Go fornece um poderoso detector de corridas para capturar corridas de dados em testes paralelos. Sempre execute seus testes paralelos com a bandeira -race durante o desenvolvimento:
go test -race ./...
O detector de corridas informará qualquer acesso concorrente à memória compartilhada sem sincronização adequada. Isso é essencial para capturar bugs sutis que podem aparecer apenas sob condições de timing específicas.
Exemplo de condição de corrida:
var counter int // Estado compartilhado - PERIGOSO!
func TestIncrement(t *testing.T) {
tests := []struct {
name string
want int
}{
{"teste1", 1},
{"teste2", 2},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
counter++ // CONDIÇÃO DE CORRIDA!
if counter != tt.want {
t.Errorf("counter = %d, want %d", counter, tt.want)
}
})
}
}
Executando isso com -race detectará a modificação concorrente de counter. A correção é tornar cada teste independente usando variáveis locais em vez de estado compartilhado.
Benefícios de Desempenho
A execução paralela pode reduzir significativamente o tempo de execução do conjunto de testes. A aceleração depende de:
- Número de núcleos de CPU: Mais núcleos permitem que mais testes sejam executados simultaneamente
- Características dos testes: Testes limitados por E/S beneficiam-se mais do que testes limitados por CPU
- Contagem de testes: Conjuntos de testes maiores veem maior economia de tempo absoluta
Medindo o desempenho:
# Execução sequencial
go test -parallel 1 ./...
# Execução paralela (padrão)
go test ./...
# Paralelismo personalizado
go test -parallel 8 ./...
Para conjuntos de testes com muitas operações de E/S (consultas ao banco de dados, requisições HTTP, operações de arquivo), você pode frequentemente obter uma aceleração de 2 a 4 vezes em sistemas modernos com múltiplos núcleos. Testes limitados por CPU podem ver menos benefícios devido à competição por recursos de CPU.
Controle de Paralelismo
Você tem várias opções para controlar a execução paralela de testes:
1. Limitar o número máximo de testes paralelos:
go test -parallel 4 ./...
2. Definir GOMAXPROCS:
GOMAXPROCS=2 go test ./...
3. Execução paralela seletiva:
Marque apenas testes específicos com t.Parallel(). Testes sem essa chamada são executados sequencialmente, o que é útil quando alguns testes devem ser executados em ordem ou compartilham recursos.
4. Execução paralela condicional:
func TestExpensive(t *testing.T) {
if testing.Short() {
t.Skip("pulando teste caro no modo curto")
}
t.Parallel()
// Lógica do teste caro
}
Padrões Comuns e Boas Práticas
Padrão 1: Configuração Antes da Execução Paralela
Se você precisar de uma configuração compartilhada por todos os casos de teste, faça-a antes do loop:
func TestWithSetup(t *testing.T) {
// Código de configuração é executado uma vez, antes da execução paralela
db := setupTestDatabase(t)
defer db.Close()
tests := []struct {
name string
id int
}{
{"usuário1", 1},
{"usuário2", 2},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Cada teste usa db de forma independente
user := db.GetUser(tt.id)
// Lógica de teste...
})
}
}
Padrão 2: Configuração por Teste
Para testes que precisam de configuração isolada, faça-a dentro de cada subteste:
func TestWithPerTestSetup(t *testing.T) {
tests := []struct {
name string
data string
}{
{"teste1", "dados1"},
{"teste2", "dados2"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Cada teste obtém sua própria configuração
tempFile := createTempFile(t, tt.data)
defer os.Remove(tempFile)
// Lógica de teste...
})
}
}
Padrão 3: Misto de Execução Sequencial e Paralela
Você pode misturar testes sequenciais e paralelos no mesmo arquivo:
func TestSequential(t *testing.T) {
// Nenhum t.Parallel() - executa sequencialmente
// Bom para testes que devem ser executados em ordem
}
func TestParallel(t *testing.T) {
tests := []struct{ name string }{{"teste1"}, {"teste2"}}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Esses executam em paralelo
})
}
}
Quando NÃO Usar a Execução Paralela
A execução paralela nem sempre é apropriada. Evite-a quando:
- Testes compartilham estado: Variáveis globais, singleton ou recursos compartilhados
- Testes modificam arquivos compartilhados: Arquivos temporários, bancos de dados de teste ou arquivos de configuração
- Testes dependem da ordem de execução: Alguns testes devem ser executados antes de outros
- Testes já são rápidos: A sobrecarga da paralelização pode exceder os benefícios
- Restrições de recursos: Testes consomem muito memória ou CPU quando paralelizados
Para testes relacionados a bancos de dados, considere usar rollbacks de transações ou bancos de dados de teste separados por teste. Nosso guia sobre padrões de banco de dados multi-tenant no Go aborda estratégias de isolamento que funcionam bem com testes paralelos.
Avançado: Testando Código Concorrente
Quando testa código concorrente (não apenas executando testes em paralelo), você precisa de técnicas adicionais:
func TestConcurrentOperation(t *testing.T) {
tests := []struct {
name string
goroutines int
}{
{"2 goroutines", 2},
{"10 goroutines", 10},
{"100 goroutines", 100},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var wg sync.WaitGroup
results := make(chan int, tt.goroutines)
for i := 0; i < tt.goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
results <- performOperation()
}()
}
wg.Wait()
close(results)
// Verificar resultados
count := 0
for range results {
count++
}
if count != tt.goroutines {
t.Errorf("esperado %d resultados, obteve %d", tt.goroutines, count)
}
})
}
}
Sempre execute tais testes com -race para detectar corridas de dados no código sob teste.
Integração com CI/CD
Testes paralelos se integram de forma suave com pipelines de CI/CD. A maioria dos sistemas de CI fornece múltiplos núcleos de CPU, tornando a execução paralela altamente benéfica:
# Exemplo GitHub Actions
- name: Executar testes
run: |
go test -race -coverprofile=coverage.out -parallel 4 ./...
go tool cover -html=coverage.out -o coverage.html
A bandeira -race é especialmente importante em CI para capturar bugs de concorrência que podem não aparecer no desenvolvimento local.
Depuração de Falhas em Testes Paralelos
Quando testes paralelos falham intermitentemente, a depuração pode ser desafiadora:
- Execute com
-race: Identifique corridas de dados - Reduza a paralelismo:
go test -parallel 1para ver se as falhas desaparecem - Execute testes específicos:
go test -run TestNamepara isolar o problema - Adicione logs: Use
t.Log()para rastrear a ordem de execução - Verifique por estado compartilhado: Procure por variáveis globais, singletons ou recursos compartilhados
Se os testes passam sequencialmente, mas falham em paralelo, você provavelmente tem uma condição de corrida ou problema de estado compartilhado.
Exemplo Prático: Testando Manipuladores HTTP
Aqui está um exemplo prático de testes de manipuladores HTTP em paralelo:
func TestHTTPHandlers(t *testing.T) {
router := setupRouter()
tests := []struct {
name string
method string
path string
statusCode int
}{
{"GET usuários", "GET", "/users", 200},
{"GET usuário por ID", "GET", "/users/1", 200},
{"POST usuário", "POST", "/users", 201},
{"DELETE usuário", "DELETE", "/users/1", 204},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
req := httptest.NewRequest(tt.method, tt.path, nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != tt.statusCode {
t.Errorf("esperado status %d, obteve %d", tt.statusCode, w.Code)
}
})
}
}
Cada teste usa httptest.NewRecorder(), que cria um gravador de resposta isolado, tornando esses testes seguros para execução paralela.
Conclusão
A execução paralela de testes orientados por tabela é uma técnica poderosa para reduzir o tempo de execução do conjunto de testes no Go. A chave para o sucesso é compreender a necessidade de captura de variáveis de loop, garantir a independência dos testes e usar o detector de corridas para capturar problemas de concorrência cedo.
Lembre-se:
- Sempre capture variáveis de loop:
tt := ttantes det.Parallel() - Garanta que os testes sejam independentes, sem estado compartilhado
- Execute os testes com
-racedurante o desenvolvimento - Controle a paralelismo com a bandeira
-parallelquando necessário - Evite a execução paralela para testes que compartilham recursos
Ao seguir essas práticas, você pode aproveitar com segurança a execução paralela para acelerar seus conjuntos de testes, mantendo a confiabilidade. Para mais padrões de teste no Go, veja nosso guia abrangente sobre testes unitários no Go, que aborda testes orientados por tabela, mocks e outras técnicas essenciais de teste.
Ao construir aplicações Go maiores, essas práticas de teste se aplicam a diferentes domínios. Por exemplo, ao construir aplicações CLI com Cobra & Viper, você usará padrões semelhantes de testes paralelos para testar manipuladores de comandos e análise de configuração.
Links Úteis
- Folha de Dicas do Go
- Testes Unitários no Go: Estrutura & Boas Práticas
- Construindo Aplicações CLI no Go com Cobra & Viper
- Padrões de Banco de Dados Multi-tenant com exemplos no Go
- Comparando ORMs do Go para PostgreSQL: GORM vs Ent vs Bun vs sqlc