Capítulo 15: Testes Unitários e de Integração com httptest e Testify
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Suíte de Testes: go test ./..."] --> B["gin.SetMode(gin.TestMode)"]
B --> C["Instanciação do Roteador Gin: gin.New()"]
subgraph MOCKING["Isolamento de Dependências (Testify Mock)"]
D["MockRepository (Embutindo mock.Mock)"]
E["mockRepo.On('BuscarPorID', 1).Return(&Produto{}, nil)"]
D --> E
end
C --> F["httptest.NewRequest(GET, '/produtos/1', nil)"]
C --> G["httptest.NewRecorder() [Buffer de Resposta em RAM]"]
F --> H["router.ServeHTTP(w, req) [Execução Direta sem Sockets TCP]"]
G --> H
H --> I{"Asserções Automatizadas (Testify Assert / Require)"}
I --> J["assert.Equal(t, 200, w.Code)"]
I --> K["assert.JSONEq(t, jsonEsperado, w.Body.String())"]
I --> L["mockRepo.AssertExpectations(t)"]
subgraph PROFILING["Garantias de Engenharia em Go"]
M["go test -race (Detector de Condições de Corrida)"]
N["go test -bench=. -benchmem (Alocações e Throughput)"]
end
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style G fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style H fill:#e0f2f1,stroke:#009688,stroke-width:2px
style I fill:#fffde7,stroke:#fbc02d,stroke-width:2px
style J fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style K fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style L fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style M fill:#d1c4e9,stroke:#512da8,stroke-width:2px
style N fill:#d1c4e9,stroke:#512da8,stroke-width:2px
🏛️ 1. Testes de Handlers HTTP sem Rede com net/http/httptest
Em muitas linguagens de programação, testar uma API REST exige subir um processo de servidor real em uma porta TCP livre e disparar chamadas de rede reais através de bibliotecas de cliente. Em Go, o pacote nativo net/http/httptest elimina essa sobrecarga através da execução direta pela interface http.Handler.
Como o *gin.Engine implementa a assinatura padrão ServeHTTP(ResponseWriter, *Request), podemos testar toda a cadeia de middlewares, validações de payload e regras de roteamento em microssegundos:
httptest.NewRecorder(): Implementa umhttp.ResponseWriterfalso que grava os bytes de cabeçalhos, cookies e corpo da resposta diretamente em um buffer de memória.httptest.NewRequest(method, url, body): Constrói uma struct*http.Requestsintética configurada para o teste.- Zero I/O de Rede: Não há alocação de portas TCP no sistema operacional, permitindo executar milhares de testes simultâneos em pipelines CI/CD sem risco de colisão de portas.
Comparativo da Pirâmide de Testes em Go
| Tipo de Teste | Alvo do Teste | Dependências Externas | Velocidade Típica |
|---|---|---|---|
| Unitário de Serviço | Lógica de Negócio pura | Mocks de interface (testify/mock) |
~0.001 ms / teste |
| Unitário de Handler | Validação HTTP, Bind JSON, Status Codes | httptest.ResponseRecorder + Mocks |
~0.05 ms / teste |
| Integração com Banco | Queries GORM, Migrações e Constraints | SQLite em RAM (file::memory:) ou Testcontainers |
~10 ms / teste |
| End-to-End (E2E) | Fluxo completo do sistema | Containers reais de Banco, Redis e RabbitMQ | ~100 ms / teste |
⚙️ 2. O Padrão Canônico Table-Driven Tests com Gin
A convenção idiomática mais importante da comunidade Go para testes unitários é o padrão Table-Driven Tests (Testes Baseados em Tabela). Uma lista de structs declara todos os cenários possíveis (casos de sucesso, entradas inválidas, erros de autenticação) e uma função t.Run() itera sobre eles de forma organizada:
package handlers_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"meu-modulo/handlers"
"meu-modulo/models"
)
func TestCadastrarProduto_TableDriven(t *testing.T) {
gin.SetMode(gin.TestMode)
// Tabela de Cenários de Teste
cenarios := []struct {
nomeCenario string
payload map[string]interface{}
statusEsperado int
validaResposta func(t *testing.T, corpoResposta string)
}{
{
nomeCenario: "Sucesso: payload válido",
payload: map[string]interface{}{
"nome": "Monitor Curvo 34",
"preco": 2499.90,
"sku": "MON-3401",
},
statusEsperado: http.StatusCreated,
validaResposta: func(t *testing.T, corpo string) {
assert.Contains(t, corpo, `"nome":"Monitor Curvo 34"`)
},
},
{
nomeCenario: "Falha: preço negativo",
payload: map[string]interface{}{
"nome": "Cadeira Gamer",
"preco": -50.00,
"sku": "CAD-0012",
},
statusEsperado: http.StatusBadRequest,
validaResposta: func(t *testing.T, corpo string) {
assert.Contains(t, corpo, "validation")
},
},
{
nomeCenario: "Falha: SKU ausente",
payload: map[string]interface{}{
"nome": "Mouse sem Fio",
"preco": 89.90,
},
statusEsperado: http.StatusBadRequest,
validaResposta: func(t *testing.T, corpo string) {
assert.Contains(t, corpo, "sku")
},
},
}
for _, cenario := range cenarios {
t.Run(cenario.nomeCenario, func(t *testing.T) {
r := gin.New()
// Registra a rota utilizando o handler real
r.POST("/produtos", handlers.CadastrarProduto)
corpoJSON, _ := json.Marshal(cenario.payload)
req := httptest.NewRequest(http.MethodPost, "/produtos", bytes.NewBuffer(corpoJSON))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, cenario.statusEsperado, w.Code)
cenario.validaResposta(t, w.Body.String())
})
}
}
🛡️ 3. O Detector de Corridas de Dados (go test -race)
Go possui um detector nativo de condições de corrida (Race Detector) integrado diretamente na ferramenta de compilação e teste, baseado na biblioteca ThreadSanitizer:
go test -race -v ./...
Se duas goroutines tentarem acessar a mesma variável de memória concorrentemente e ao menos uma delas executar uma operação de escrita sem o devido lock (sync.Mutex ou canal de comunicação), o comando -race aborta imediatamente e imprime o stack trace exato das duas goroutines envolvidas. Em pipelines CI/CD profissionais, nenhum código Go deve ser mergeado para a branch main sem passar limpo pelo teste com flag -race.
⚡ 4. Benchmarking e Perfilamento de Memória (testing.B)
Para garantir que uma refatoração em um endpoint crítico não introduziu alocações desnecessárias na memória heap:
func BenchmarkCadastrarProdutoHandler(b *testing.B) {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.POST("/produtos", handlers.CadastrarProduto)
payload := []byte(`{"nome":"Teclado","preco":150.0,"sku":"TEC-1000"}`)
b.ResetTimer()
b.ReportAllocs() // Mede alocações de memória por operação (allocs/op)
for i := 0; i < b.N; i++ {
req := httptest.NewRequest(http.MethodPost, "/produtos", bytes.NewReader(payload))
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
}
}
Execução:
go test -bench=. -benchmem ./handlers/...