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.
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.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.