Guia de TDD com Claude Code: desenvolvimento orientado a testes com Vitest e node:test
Pratique TDD com Claude Code usando exemplos de Vitest, node:test, CI, hooks e templates de prompt.
Claude Code consegue escrever código muito rápido, mas velocidade não é a mesma coisa que segurança. Os problemas costumam aparecer depois: um caso de borda esquecido, uma regressão, um desconto calculado errado ou um teste que só falha no CI.
TDD, ou desenvolvimento orientado a testes, ajuda a controlar essa velocidade. O ciclo é simples: escrever primeiro um teste que falha, fazer a menor implementação que o torna verde e depois refatorar sem mudar o comportamento. Esse ciclo é conhecido como Red-Green-Refactor.
Claude Code funciona bem nesse fluxo porque consegue listar casos, ler erros, implementar mudanças pequenas e ajustar CI. O cuidado é não pedir apenas “crie a funcionalidade”. Peça primeiro a prova da falha. Este artigo traz exemplos copiáveis com Vitest e node:test, CI, hooks atuais do Claude Code e templates de prompt.
Para esta atualização, consultei a documentação oficial de Claude Code hooks, Claude Code memory, Claude Code settings, Vitest Getting Started, Vitest CLI e Node.js test runner. O exemplo de hook lê JSON pelo stdin e usa tool_input.file_path, alinhado com a documentação atual.
Onde Claude Code ajuda no TDD
Claude Code pode escrever testes, interpretar falhas, fazer a implementação mínima, configurar CI e resumir riscos. A pessoa responsável ainda define regras de negócio, segurança, contrato de API e decisão de publicação.
| Etapa | Tarefa para Claude Code | Revisão humana |
|---|---|---|
| Red | Escrever testes que falham | Ele inventou requisitos? |
| Green | Implementar o mínimo | Há abstração desnecessária? |
| Refactor | Melhorar nomes e duplicação | O comportamento ficou igual? |
| CI | Rodar testes em cada PR | A versão do Node é realista? |
| Operação | Usar hooks e CLAUDE.md | A automação está rápida? |
flowchart LR
A["Dividir a especificação"] --> B["Red: teste falhando"]
B --> C["Green: implementação mínima"]
C --> D["Refactor: limpeza"]
D --> E["CI e hooks rodam de novo"]
E --> B
Exemplo 1: preços e cupons com Vitest
Lógica de preço, cupons e assinaturas é um bom ponto de partida porque erros pequenos afetam receita.
npm install -D vitest
{
"type": "module",
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
},
"devDependencies": {
"vitest": "^3.0.0"
}
}
Crie primeiro src/cart.test.ts.
import { describe, expect, it } from "vitest";
import { priceCart, ValidationError } from "./cart";
describe("priceCart", () => {
it("calculates subtotal and total without a coupon", () => {
const result = priceCart({
items: [
{ sku: "book", unitPriceCents: 1200, quantity: 2 },
{ sku: "video", unitPriceCents: 3000, quantity: 1 },
],
});
expect(result).toEqual({
subtotalCents: 5400,
discountCents: 0,
totalCents: 5400,
});
});
it("applies a valid percent coupon", () => {
const result = priceCart(
{
items: [{ sku: "course", unitPriceCents: 10000, quantity: 1 }],
coupon: {
code: "SPRING20",
percentOff: 20,
expiresAt: "2026-06-30T00:00:00.000Z",
},
},
{ now: new Date("2026-06-02T00:00:00.000Z") },
);
expect(result.totalCents).toBe(8000);
expect(result.discountCents).toBe(2000);
});
it("rejects expired coupons", () => {
expect(() =>
priceCart(
{
items: [{ sku: "course", unitPriceCents: 10000, quantity: 1 }],
coupon: {
code: "OLD20",
percentOff: 20,
expiresAt: "2026-05-01T00:00:00.000Z",
},
},
{ now: new Date("2026-06-02T00:00:00.000Z") },
),
).toThrow(ValidationError);
});
it("rejects zero or negative quantity", () => {
expect(() =>
priceCart({
items: [{ sku: "book", unitPriceCents: 1200, quantity: 0 }],
}),
).toThrow("quantity must be positive");
});
});
Peça a Claude Code para confirmar o Red.
Estamos na etapa Red. src/cart.test.ts existe, mas src/cart.ts ainda não existe.
Faça:
1. Execute npm test e confirme a falha.
2. Implemente apenas o src/cart.ts mínimo para passar.
3. Não adicione UI, banco, API externa ou recursos futuros.
4. Refatore apenas depois do Green.
Implementação mínima de src/cart.ts.
export class ValidationError extends Error {
constructor(message: string) {
super(message);
this.name = "ValidationError";
}
}
type CartItem = {
sku: string;
unitPriceCents: number;
quantity: number;
};
type Coupon = {
code: string;
percentOff: number;
expiresAt: string;
};
type CartInput = {
items: CartItem[];
coupon?: Coupon;
};
type PriceOptions = {
now?: Date;
};
export function priceCart(input: CartInput, options: PriceOptions = {}) {
if (input.items.length === 0) {
throw new ValidationError("cart must contain at least one item");
}
const subtotalCents = input.items.reduce((sum, item) => {
if (!Number.isInteger(item.quantity) || item.quantity <= 0) {
throw new ValidationError("quantity must be positive");
}
if (!Number.isInteger(item.unitPriceCents) || item.unitPriceCents < 0) {
throw new ValidationError("unitPriceCents must be a non-negative integer");
}
return sum + item.unitPriceCents * item.quantity;
}, 0);
const discountCents = calculateDiscount(subtotalCents, input.coupon, options.now ?? new Date());
return {
subtotalCents,
discountCents,
totalCents: subtotalCents - discountCents,
};
}
function calculateDiscount(subtotalCents: number, coupon: Coupon | undefined, now: Date) {
if (!coupon) return 0;
if (coupon.percentOff <= 0 || coupon.percentOff > 100) {
throw new ValidationError("percentOff must be between 1 and 100");
}
if (new Date(coupon.expiresAt).getTime() < now.getTime()) {
throw new ValidationError("coupon expired");
}
return Math.round(subtotalCents * (coupon.percentOff / 100));
}
Exemplo 2: limites de CLI com node:test
Para utilitários pequenos em Node, node:test reduz dependências. Salve como limit.test.mjs.
import test from "node:test";
import assert from "node:assert/strict";
export function parseLimit(value, fallback = 20) {
if (value === undefined || value === "") return fallback;
const parsed = Number(value);
if (!Number.isInteger(parsed)) {
throw new TypeError("limit must be an integer");
}
if (parsed < 1 || parsed > 100) {
throw new RangeError("limit must be between 1 and 100");
}
return parsed;
}
test("parseLimit uses fallback when the value is empty", () => {
assert.equal(parseLimit(undefined), 20);
assert.equal(parseLimit("", 50), 50);
});
test("parseLimit accepts values from 1 to 100", () => {
assert.equal(parseLimit("1"), 1);
assert.equal(parseLimit("100"), 100);
});
test("parseLimit rejects decimals and out-of-range values", () => {
assert.throws(() => parseLimit("1.5"), /integer/);
assert.throws(() => parseLimit("0"), /between 1 and 100/);
assert.throws(() => parseLimit("101"), /between 1 and 100/);
});
node --test limit.test.mjs
Exemplo 3: bug de API como regressão
Quando um bug chega à produção, transforme-o primeiro em teste falhando.
Adicione um teste de regressão com TDD.
Contexto:
- POST /checkout aceita cupons expirados por engano.
- Cupons válidos e checkout sem cupom devem continuar funcionando.
Red:
- Adicione teste que espera 400 para cupom expirado.
- Confirme que a implementação atual falha.
Green:
- Faça a menor mudança possível na API.
Refactor:
- Extraia apenas a comparação de datas duplicada.
Retorne:
- Nome do teste, saída da falha, arquivos alterados, comandos e riscos.
Veja também o guia de testes de API e o guia de estratégia de testes.
CI e hooks
name: test
on:
pull_request:
push:
branches: [main]
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
Hook para rodar Vitest relacionado depois de edições.
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/run-related-vitest.mjs",
"timeout": 120
}
]
}
]
}
}
import { spawnSync } from "node:child_process";
import path from "node:path";
let raw = "";
for await (const chunk of process.stdin) {
raw += chunk;
}
const event = raw ? JSON.parse(raw) : {};
const filePath = event.tool_input?.file_path;
if (typeof filePath !== "string" || !/\.[cm]?[jt]sx?$/.test(filePath)) {
process.exit(0);
}
const target = path.isAbsolute(filePath)
? path.relative(process.cwd(), filePath)
: filePath;
const result = spawnSync("npx", ["vitest", "related", target, "--run"], {
stdio: "inherit",
shell: process.platform === "win32",
});
process.exit(result.status ?? 1);
Em CLAUDE.md, deixe uma regra curta.
## TDD workflow
- Behavior changes start with a failing test.
- Show the Red result before implementation.
- Implement the smallest change that makes the test pass.
- Refactor only after the targeted test is Green.
- Report the command, result, changed files, and remaining risk.
Para mais detalhes, leia o guia de hooks e as boas práticas de CLAUDE.md.
Templates de prompt
Nova funcionalidade com TDD:
Objetivo:
Adicionar [funcionalidade].
Especificação:
- [caminho feliz]
- [valores de borda]
- [comportamento de erro]
Processo:
1. Escrever testes primeiro.
2. Rodar npm test e mostrar Red.
3. Implementar o mínimo para Green.
4. Refatorar sem mudar comportamento.
Retorno:
Saída da falha, comandos, arquivos alterados e riscos.
Correção de bug com TDD:
Reprodução:
[entrada, ação ou log]
Esperado:
[comportamento correto]
Atual:
[comportamento atual]
Pedido:
Primeiro adicione um teste de regressão que falha.
Depois faça a menor correção.
Não enfraqueça nem apague testes existentes sem explicar.
Refatoração segura:
Alvo:
[arquivo/função]
Restrição:
O comportamento público não muda.
Passos:
1. Adicionar characterization tests.
2. Confirmar Green.
3. Alterar apenas estrutura interna.
4. Rodar os mesmos testes.
Armadilhas comuns
A primeira é pular o Red. Se o teste já passa antes da correção, ele não protege nada. A segunda é testar detalhes internos em vez de comportamento. A terceira é usar tempo real em teste; injete now. A quarta é confiar só em mocks para pagamento, email ou CRM. A quinta é deixar Claude Code apagar testes para chegar ao Green.
CTA
Comece com uma regra de preço, um parser de CLI ou uma regressão de API. Quem trabalha sozinho pode usar a folha gratuita de Claude Code e os templates acima. Para prompts, hooks e checklists reutilizáveis, veja os produtos ClaudeCodeLab. Equipes que precisam de TDD, CI, permissões e revisão em um repositório real podem usar treinamento e consultoria Claude Code.
Resultado ao testar
No fluxo de Masa, pedir primeiro o teste falhando reduziu o tempo de revisão. Cupom expirado, quantidade zero e rotas API sem autenticação apareceram mais cedo. Rodar todos os testes por hook ficou lento; a configuração prática passou a rodar Vitest relacionado após edições e E2E completo no CI.
Artigos relacionados
Testes de API com Claude Code: guia prático de automação
Guia prático de testes de API com Claude Code: smoke, auth, JSON, contratos, CI e exemplos executáveis.
Code review com Claude Code para equipes
Use Claude Code em reviews de PR com gates de CI, CODEOWNERS, templates e prompts baseados em evidência.
Gate de revisão antes do commit com Claude Code
Revisão antes do commit com Claude Code: diff, build, URL pública, Gumroad, consultoria, testes e arquivos fora do escopo.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.