Fundamentos de Programação/02 - Código que Presta/Semana 07 - Testes Automatizados9 min
Pirâmide de Testes
- Por que a base é larga: custo de execução ou custo de manutenção?
- O que é o antipadrão do "cone de sorvete" (tudo end-to-end)?
- Se você só pudesse ter uma camada, qual escolheria para o seu projeto atual?
- Onde a pirâmide falha como metáfora?
Conceito
A pirâmide diz: muitos testes unitários, menos de integração, poucos end-to-end.
Por que a base é larga — e a resposta popular ("porque unitário é rápido") é só metade:
| Razão | O que significa |
|---|---|
| velocidade | ciclo de feedback. Suíte de 3 segundos você roda a cada salvamento; de 8 minutos, você roda no CI e descobre 40 minutos depois |
| localização do defeito | um unitário que falha aponta uma função. Um E2E que falha aponta "algo no sistema" |
| custo de manutenção | teste E2E quebra por motivos alheios ao seu código: rede, dado de fixture, timing, ordem |
A segunda é a mais subestimada. O valor de um teste não está só em detectar a falha — está em dizer onde ela está. Uma suíte que só te diz "quebrou" transfere para você o trabalho que ela deveria ter feito.
O antipadrão: o cone de sorvete. Poucos unitários, muitos E2E. Sintomas, e todos aparecem juntos:
- suíte lenta → ninguém roda localmente
- instável (flaky) → falhas são ignoradas por hábito
- falha não localiza → cada vermelho custa uma investigação
- e a consequência final: a suíte deixa de ser confiável, e a equipe para de confiar nela — momento em que ela passa a custar sem entregar
A pirâmide também é criticada, com razão. Duas alternativas conhecidas:
- Testing Trophy (Kent C. Dodds) — a maior fatia é integração, porque em aplicação web ela dá a melhor confiança por unidade de custo
- Honeycomb (Spotify) — foco em testes de contrato entre serviços, em vez de unidade
E as críticas são justas: a pirâmide não diz quais comportamentos testar, e supõe que "unitário é sempre barato" — falso quando a unidade exige montagem pesada.
A pergunta melhor que "qual proporção":
Para cada camada: que classe de bug ela pega que a camada de baixo não pega?
Se a resposta é "nenhuma", a camada é redundante. Se é "acordo entre partes", é integração. Se é "o sistema todo em pé", é E2E. A proporção sai disso, não de um desenho.
Em Go
Go achata a pirâmide, e por razões concretas — não por preferência cultural:
| Fator | Efeito |
|---|---|
| compilação rápida | o ciclo é curto mesmo com muitos testes |
net/http/httptest na stdlib |
teste de integração HTTP roda em microssegundos |
| nenhum container de framework para subir | não existe o "boot do Spring" de 20s |
| binário único, sem servidor de aplicação | E2E é subir um processo |
go test cacheia por pacote |
pacote inalterado não roda de novo |
O resultado: em Go, um teste de integração pode ser mais rápido que um teste unitário de Java. Isso muda a economia — a fatia do meio fica legitimamente maior, e o Testing Trophy descreve um projeto Go melhor que a pirâmide clássica.
Números que ajudam a calibrar: a stdlib de Go testa net/http com centenas de testes
que sobem servidores reais. Se a biblioteca padrão trata isso como rotina, o seu projeto
também pode.
Ferramentas de execução que importam para a forma da pirâmide:
| Comando | Efeito |
|---|---|
go test ./... |
roda pacotes em paralelo |
go test -short ./... |
pula os que você marcou como lentos |
go test -tags=integration ./... |
inclui os que só compilam com a tag |
go test -race ./... |
detector de corrida — no CI, sempre |
go test -count=1 ./... |
ignora o cache (útil para caçar teste instável) |
go test -count=10 -run TestX |
roda 10 vezes: é assim que se prova que não é flaky |
O -race merece destaque: é um detector de data race de qualidade industrial, embutido,
que roda com um comando. Ele pega uma classe de bug que nenhuma camada da pirâmide pega
por design.
Respostas às perguntas-guia
1. Por que a base é larga: custo de execução ou custo de manutenção?
Conceito: os dois, mais um terceiro que é o mais importante — localização do defeito. Unitário diz onde; E2E diz que quebrou.
Em Go: o custo de execução é muito menor que a média, então manutenção e localização dominam a decisão. É por isso que a proporção idiomática em Go é diferente.
2. O que é o antipadrão do "cone de sorvete" (tudo end-to-end)?
Conceito: poucos unitários e muitos E2E: lento, instável, não localiza, e termina em suíte que ninguém confia.
Em Go: o sintoma específico é a suíte que só passa com -p 1 e falha aleatoriamente no
CI — sinal de estado compartilhado entre testes, que é o que E2E acumula.
3. Se você só pudesse ter uma camada, qual escolheria para o seu projeto atual?
Conceito: depende de onde estão seus bugs. Para API HTTP com banco, a fatia de integração dá a melhor confiança por custo.
Em Go, concretamente: httptest.NewServer(seuRouter) + banco de teste, exercitando as
rotas principais. Isso pega roteamento, serialização, SQL e status code de uma vez, e
custa milissegundos. Se eu pudesse ter só uma camada num serviço Go, seria essa.
4. Onde a pirâmide falha como metáfora?
Conceito: ela não diz o que testar, supõe que unitário é sempre barato, e sugere que proporção é o objetivo — quando o objetivo é cobertura de classes de risco.
Em Go: falha também porque httptest desloca a fronteira: metade dos testes que a
pirâmide classificaria como "integração lenta" em Go são rápidos. A metáfora foi desenhada
para um mundo onde subir o contexto da aplicação custava dezenas de segundos.
Trade-offs
Do conceito:
- Muitos unitários compram feedback e localização; cobram acoplamento ao desenho interno e não pegam desacordo entre partes.
- Muitos de integração compram confiança realista; cobram infraestrutura e tempo.
- E2E compra a única prova de que o sistema funciona de ponta a ponta; cobra instabilidade e diagnósticos ruins. Poucos e bem escolhidos.
Em Go:
httptestbarato empurra para o meio da pirâmide — e o risco é negligenciar o unitário da regra de negócio pura, que é onde a lógica difícil mora.- Cache de teste por pacote acelera muito e esconde teste instável (use
-count=1para verificar). - Pacotes em paralelo é ganho grande e exige que os testes sejam de fato isolados.
Exemplo prático
A mesma regra, verificada nas três camadas — e o que só cada camada pega:
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// ==================== a regra: pura, sem I/O ====================
type Centavos int64
func Frete(pesoGramas int, uf string) (Centavos, error) {
if pesoGramas <= 0 {
return 0, fmt.Errorf("peso %d inválido", pesoGramas)
}
base := Centavos(1500)
if pesoGramas > 5000 {
base += Centavos((pesoGramas - 5000) / 1000 * 200)
}
switch uf {
case "SP", "RJ", "MG":
return base, nil
case "AM", "AC", "RR":
return base * 2, nil
default:
return base * 3 / 2, nil
}
}
// ==================== a fronteira HTTP ====================
type respostaFrete struct {
Valor Centavos `json:"valor_centavos"`
}
func handlerFrete(w http.ResponseWriter, r *http.Request) {
var req struct {
Peso int `json:"peso_gramas"`
UF string `json:"uf"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "json inválido", http.StatusBadRequest)
return
}
v, err := Frete(req.Peso, strings.ToUpper(req.UF))
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(respostaFrete{Valor: v})
}
// ============ CAMADA 1: unitário. Pega erro de REGRA. ============
// Barato, rápido, e localiza o defeito numa função. É onde a lógica difícil
// deve ser exercitada exaustivamente.
func TestFrete_Regra(t *testing.T) {
casos := []struct {
peso int
uf string
want Centavos
}{
{1000, "SP", 1500},
{1000, "AM", 3000},
{1000, "BA", 2250},
{8000, "SP", 2100}, // 1500 + 3*200
{8000, "AM", 4200},
}
for _, c := range casos {
t.Run(fmt.Sprintf("%dg/%s", c.peso, c.uf), func(t *testing.T) {
got, err := Frete(c.peso, c.uf)
if err != nil {
t.Fatalf("err = %v", err)
}
if got != c.want {
t.Errorf("Frete(%d,%q) = %d; want %d", c.peso, c.uf, got, c.want)
}
})
}
if _, err := Frete(0, "SP"); err == nil {
t.Error("peso 0 deveria dar erro")
}
}
// ============ CAMADA 2: integração. Pega erro de FRONTEIRA. ============
// A regra pode estar perfeita e isto falhar: tag json errada, status code
// errado, uf não normalizada, corpo inválido tratado como 500.
func TestFrete_HTTP(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(handlerFrete))
t.Cleanup(srv.Close)
casos := []struct {
nome, corpo string
wantStatus int
wantValor Centavos
}{
{"ok", `{"peso_gramas":1000,"uf":"SP"}`, 200, 1500},
{"uf minúscula é normalizada", `{"peso_gramas":1000,"uf":"sp"}`, 200, 1500},
{"peso inválido é 422", `{"peso_gramas":0,"uf":"SP"}`, 422, 0},
{"json quebrado é 400", `{peso`, 400, 0},
}
for _, c := range casos {
t.Run(c.nome, func(t *testing.T) {
resp, err := http.Post(srv.URL, "application/json", strings.NewReader(c.corpo))
if err != nil {
t.Fatalf("POST: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != c.wantStatus {
t.Fatalf("status = %d; want %d", resp.StatusCode, c.wantStatus)
}
if c.wantStatus != 200 {
return
}
// verifica o JSON NO FIO: o nome da chave é contrato público
var bruto map[string]any
if err := json.NewDecoder(resp.Body).Decode(&bruto); err != nil {
t.Fatalf("json: %v", err)
}
v, ok := bruto["valor_centavos"]
if !ok {
t.Fatalf("chave valor_centavos ausente; veio %v", bruto)
}
if Centavos(v.(float64)) != c.wantValor {
t.Errorf("valor = %v; want %d", v, c.wantValor)
}
})
}
}
// ============ CAMADA 3: E2E. Pega erro de FIAÇÃO. ============
// Poucos, e só os caminhos que precisam provar que o sistema está de pé.
// Aqui: o roteador registra a rota no caminho certo?
func TestFrete_E2E_RotaRegistrada(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("POST /api/v1/frete", handlerFrete) // roteamento de verdade
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
// caminho certo: 200
resp, _ := http.Post(srv.URL+"/api/v1/frete", "application/json",
strings.NewReader(`{"peso_gramas":1000,"uf":"SP"}`))
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Errorf("rota correta deu %d", resp.StatusCode)
}
// caminho errado: 404. Nenhum unitário pegaria uma rota registrada errada.
resp2, _ := http.Post(srv.URL+"/frete", "application/json",
strings.NewReader(`{"peso_gramas":1000,"uf":"SP"}`))
defer resp2.Body.Close()
if resp2.StatusCode != 404 {
t.Errorf("rota inexistente deu %d; want 404", resp2.StatusCode)
}
// método errado: 405. Também invisível ao unitário.
resp3, _ := http.Get(srv.URL + "/api/v1/frete")
defer resp3.Body.Close()
if resp3.StatusCode != http.StatusMethodNotAllowed {
t.Errorf("GET deu %d; want 405", resp3.StatusCode)
}
}
Leia as três camadas pelo que só elas pegam: a 1 pega 8000g/SP = 2100 errado; a 2
pega a chave JSON renomeada e o 422 virando 500; a 3 pega a rota registrada como
/api/frete em vez de /api/v1/frete. Nenhuma substitui a outra — e é essa pergunta, não
a proporção, que define a forma da sua pirâmide.
Relacionado
- 1. Teste Unitário — a base
- 2. Teste de Integração — o meio, que em Go é mais barato que a média
- 4. Mocks e Stubs — o que dubla o que, em cada camada
- 5. TDD — a pirâmide descreve a suíte; TDD descreve como ela nasce
Parte de Semana 07 - Testes Automatizados · 00 - MOC Fundamentos de Programação