trilha

Fundamentos de Programação/02 - Código que Presta/Semana 07 - Testes Automatizados9 min

TDD

Perguntas-guia
  • 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:

  1. Red — escreva um teste que falha. Rode e veja falhar.
  2. Green — faça passar do jeito mais simples que existir, inclusive feio.
  3. 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.

Por que escrever o teste primeiro muda o design, e não só a cobertura

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:

  1. {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.
  2. TestRomano_Propriedades verifica 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


Parte de Semana 07 - Testes Automatizados · 00 - MOC Fundamentos de Programação

Buscar

Busca por título, seção e texto das notas