Fundamentos de Programação/02 - Código que Presta/Semana 07 - Testes Automatizados9 min
TDD
- Red-green-refactor: qual etapa as pessoas mais pulam, e o que se perde nela?
- Por que escrever o teste primeiro muda o design, não só a cobertura?
- Onde TDD atrapalha (exploração, spike, UI)?
- Você consegue escrever teste antes do código uma vez sem sofrer? (item da autoavaliação)
Conceito
Red-green-refactor:
- Red — escreva um teste que falha. Rode e veja falhar.
- Green — faça passar do jeito mais simples que existir, inclusive feio.
- Refactor — melhore a estrutura, com o teste te protegendo.
Qual etapa as pessoas mais pulam? Duas, e cada uma custa uma coisa diferente:
Pulam o refactor. É a mais citada, e a consequência é que TDD produz código que funciona e não melhora. Quem pula essa etapa conclui, com razão a partir da própria experiência, que "TDD não ajuda no design" — mas o design vinha justamente da etapa pulada.
Pulam ver o vermelho. Menos citada e mais perigosa. Se você não vê o teste falhar, você não sabe que ele é capaz de falhar. Testes que não testam nada existem em quantidade: asserção invertida, teste que nunca roda por nome errado, mock que engole a verificação. Ver o vermelho é o único momento em que você valida o teste.
Escrevendo o teste antes, você é obrigado a usar a API antes dela existir — e do ponto de vista de quem chama.
Se ela é desconfortável de chamar, você descobre agora, quando mudar custa uma linha, em vez de descobrir depois de vinte chamadas espalhadas.
E há um segundo efeito, mais forte: código difícil de testar é código com dependências demais ou responsabilidade confusa. Ao sentir a dificuldade no momento em que ela nasce, você corrige o design, não o teste. TDD é uma técnica de projeto que produz testes como subproduto — quem a trata como técnica de produção de testes perde o principal.
Onde TDD atrapalha, e vale ser honesto:
- exploração / spike — você não sabe o design ainda; testar cada tentativa é atrito puro. Explore, jogue fora, depois faça TDD
- UI — a asserção verdadeira é "parece certo", e isso não é automatizável
- código de integração fina — um wrapper de três linhas em volta de uma chamada externa: o teste seria uma cópia do wrapper
- algoritmo cuja especificação você está descobrindo — TDD pressupõe saber o que "correto" significa para o próximo caso
Em Go
Go tem o ciclo mais curto entre as linguagens compiladas, e isso é o que torna TDD prático — não uma preferência:
go test -run TestFrete ./... # roda UM teste
Compilar + rodar um teste em Go é questão de centenas de milissegundos.
Em Go, "não compila" é o primeiro vermelho legítimo. Você escreve o teste chamando uma
função que ainda não existe; go test falha com
undefined: Frete. Esse é o red. Nada de escrever um esqueleto antes: o erro de
compilação já te diz que o teste está exercendo algo que falta.
Essa é uma diferença real com linguagens dinâmicas, onde o primeiro vermelho é um
NameError em runtime — em Go o compilador te dá o vermelho antes de qualquer execução.
Table-driven test casa perfeitamente com TDD (1. Teste Unitário): o ciclo se torna adicione uma linha na tabela → veja falhar → faça passar. A tabela é a lista de requisitos, crescendo caso a caso.
Ferramentas para o loop apertado:
| Comando | Uso |
|---|---|
go test -run 'TestX/caso' ./pkg |
o caso exato em que você está |
go test -failfast ./... |
para no primeiro vermelho |
gotestsum --watch |
re-roda ao salvar |
go test -count=1 |
ignora o cache (o cache pode te dar verde falso durante TDD) |
go test -race |
antes de commitar, sempre |
O cache merece atenção durante TDD: go test cacheia resultados por pacote. Se você
mudou algo que o Go não considera relevante, pode ver um verde antigo. -count=1 desliga.
Respostas às perguntas-guia
1. Red-green-refactor: qual etapa as pessoas mais pulam, e o que se perde nela?
Conceito: o refactor (perde-se a melhoria de design — e é dele que vem o benefício principal) e ver o vermelho (perde-se a validação de que o teste funciona).
Em Go: pular o vermelho é fácil porque a suíte é rápida e você tende a rodar tudo de uma
vez, vendo só o verde final. Use -run para isolar e olhar o vermelho de propósito.
2. Por que escrever o teste primeiro muda o design, não só a cobertura?
Conceito: porque você usa a API antes de existir, como cliente, e sente o atrito na hora em que ele é barato de corrigir. Dificuldade de testar é sintoma de dependências demais.
Em Go, o sintoma aparece de forma bem concreta: se o teste precisa de três dublês, a função tem três colaboradores. A correção é extrair a regra pura — que não precisa de dublê nenhum (4. Mocks e Stubs).
3. Onde TDD atrapalha (exploração, spike, UI)?
Conceito: quando você não sabe o design, quando a asserção é subjetiva, ou quando o teste seria uma cópia do código.
Em Go: wrapper fino em volta de uma chamada externa é o caso mais claro — o teste unitário dele seria um mock devolvendo o que você mandou. Teste isso na integração (2. Teste de Integração).
4. Você consegue escrever teste antes do código uma vez sem sofrer?
Este é um item da Autoavaliação, e o exercício abaixo é um ciclo completo. O critério não é "gostei" — é ter passado por red → green → refactor vendo cada etapa, uma vez.
Trade-offs
Do conceito:
- TDD compra design testável e cobertura como subproduto; cobra disciplina e é lento quando você ainda não sabe o que está construindo.
- Escrever teste depois compra velocidade inicial; cobra que o design já esteja fixado quando você descobrir que ele é difícil de testar.
- Green "do jeito mais simples" compra um passo seguro e exige o refactor depois — sem ele, você acumula código deliberadamente feio.
Em Go:
- Ciclo rápido e compilador estrito tornam TDD confortável; o "não compila" como primeiro vermelho é um ganho real.
- Table-driven casa bem com TDD e empurra para casos que caibam na mesma forma.
- O cache de teste pode dar verde falso no meio do ciclo.
Exemplo prático
Um ciclo completo, com os passos registrados como eles aconteceram. O requisito: converter número em algarismos romanos.
package main
import (
"fmt"
"strings"
"testing"
)
// ============================================================
// CICLO 1 — RED: o teste chama uma função que não existe.
// Erro: "undefined: Romano". Em Go, ISTO é o primeiro vermelho.
// CICLO 1 — GREEN: a coisa mais simples que passa.
// func Romano(n int) string { return "I" }
// Sim, hardcoded. É green, e é permitido.
// ============================================================
// ============================================================
// CICLO 2 — RED: {2, "II"} falha com o hardcode.
// CICLO 2 — GREEN: repetir "I" n vezes. Passa para 1, 2, 3.
// func Romano(n int) string { return strings.Repeat("I", n) }
// ============================================================
// ============================================================
// CICLO 3 — RED: {4, "IV"} falha ("IIII").
// CICLO 3 — GREEN: caso especial para 4.
// CICLO 3 — REFACTOR: o caso especial revela o PADRÃO — pares
// (valor, símbolo) em ordem decrescente, incluindo os subtrativos.
// É aqui que o design aparece, e ele apareceu do refactor.
// ============================================================
var simbolos = []struct {
valor int
sim string
}{
{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
{100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
{10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"},
}
func Romano(n int) (string, error) {
if n <= 0 || n > 3999 {
return "", fmt.Errorf("romano: %d fora de 1..3999", n)
}
var b strings.Builder
for _, s := range simbolos {
for n >= s.valor {
b.WriteString(s.sim)
n -= s.valor
}
}
return b.String(), nil
}
// A tabela É a lista de requisitos, e ela cresceu uma linha por ciclo.
func TestRomano(t *testing.T) {
casos := []struct {
in int
want string
}{
{1, "I"}, // ciclo 1
{2, "II"}, // ciclo 2
{3, "III"}, //
{4, "IV"}, // ciclo 3: forçou o refactor
{5, "V"}, // ciclo 4
{9, "IX"}, // ciclo 5: confirmou o padrão subtrativo
{14, "XIV"}, // ciclo 6: combinação
{40, "XL"}, //
{90, "XC"}, //
{400, "CD"}, //
{1994, "MCMXCIV"}, // ciclo 7: o caso famoso, passou de graça
{3999, "MMMCMXCIX"},
}
for _, c := range casos {
t.Run(fmt.Sprint(c.in), func(t *testing.T) {
got, err := Romano(c.in)
if err != nil {
t.Fatalf("Romano(%d) erro inesperado: %v", c.in, err)
}
if got != c.want {
t.Errorf("Romano(%d) = %q; want %q", c.in, got, c.want)
}
})
}
}
// Ciclo 8 — RED: e os limites? O teste veio antes da validação existir.
func TestRomano_Limites(t *testing.T) {
for _, n := range []int{0, -1, 4000} {
if _, err := Romano(n); err == nil {
t.Errorf("Romano(%d) deveria dar erro", n)
}
}
}
// ============================================================
// A prova de que o refactor do ciclo 3 valeu: uma propriedade que
// nunca foi testada caso a caso passa para TODOS os valores.
// Teste de propriedade, não de exemplo.
// ============================================================
func TestRomano_Propriedades(t *testing.T) {
valorDe := map[byte]int{'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000}
for n := 1; n <= 3999; n++ {
r, err := Romano(n)
if err != nil {
t.Fatalf("Romano(%d): %v", n, err)
}
// propriedade 1: só símbolos válidos
for i := 0; i < len(r); i++ {
if _, ok := valorDe[r[i]]; !ok {
t.Fatalf("Romano(%d) = %q tem símbolo inválido %q", n, r, r[i])
}
}
// propriedade 2: decodificar de volta devolve o original
soma, ant := 0, 0
for i := len(r) - 1; i >= 0; i-- {
v := valorDe[r[i]]
if v < ant {
soma -= v // notação subtrativa
} else {
soma += v
ant = v
}
}
if soma != n {
t.Fatalf("Romano(%d) = %q decodifica para %d", n, r, soma)
}
// propriedade 3: nunca 4 símbolos iguais seguidos
for _, s := range []string{"IIII", "XXXX", "CCCC", "MMMM"} {
if strings.Contains(r, s) {
t.Fatalf("Romano(%d) = %q contém %q", n, r, s)
}
}
}
}
Os dois pontos que valem levar:
{1994, "MCMXCIV"}passou de graça. Ninguém programou o caso 1994 — ele caiu do design que emergiu no refactor do ciclo 3. É esse o retorno de TDD: você não escreve a solução geral, você a descobre quando o terceiro caso te força a parar de remendar.TestRomano_Propriedadesverifica 3.999 valores contra três invariantes, e passa. Isso só é possível porque a implementação ficou uniforme — o que, de novo, veio do refactor.
Relacionado
- 1. Teste Unitário — table-driven é a forma que o ciclo toma em Go
- 4. Refatorações Nomeadas — semana 6, o "refactor" do ciclo tem nomes e receitas
- 4. Mocks e Stubs — dificuldade de testar é sintoma de design, não de ferramenta
- 3. Pirâmide de Testes — TDD produz a base da pirâmide naturalmente
Parte de Semana 07 - Testes Automatizados · 00 - MOC Fundamentos de Programação