12k
All articles

TDD ainda importa quando agentes escrevem o código?

Veja como o desenvolvimento orientado a testes controla agentes de IA: escreva testes Vitest primeiro, impeça alterações e verifique o código na CI.

OpenReplay Team
OpenReplay Team
TDD ainda importa quando agentes escrevem o código?

Sim, e talvez até mais do que antes, só que como um contrato, não como um ritual. Quando um agente de código escreve a implementação, os testes escritos primeiro passam a ser a especificação. Eles definem o que é “pronto” antes de existir qualquer código e flagram o agente quando ele declara concluída uma tarefa que não está.

A maioria dos desenvolvedores que delega a implementação ao Claude Code, ao Cursor ou ao GitHub Copilot já viu isso acontecer. O agente anuncia que tudo está passando e a funcionalidade quebra no primeiro uso. Ou então um arquivo de teste foi reescrito discretamente para concordar com o código novo.

Este artigo apresenta o fluxo de trabalho test-first para agentes: um arquivo Vitest real, a instrução exata para acompanhá-lo, as formas como os agentes o contornam e os mecanismos de proteção que fecham essas brechas.

Principais pontos

  • Testes escritos antes de o agente começar funcionam como uma especificação executável: o trabalho do agente é fazê-los passar, não decidir o que significa passar.
  • Um teste escrito depois do código tende a descrever o que o código já faz. Por isso ele passa por construção e não verifica nada.
  • Uma mensagem do agente dizendo que todos os testes passam é uma alegação. Uma execução de CI no commit enviado é a evidência.
  • Se um mesmo diff altera tanto a implementação quanto as asserções que a avaliam, o agente corrigiu a própria prova. Revise as mudanças nos testes separadamente das mudanças no código.
  • Dispense o test-first em protótipos descartáveis e em ajustes puramente visuais, nos quais o resultado esperado não pode ser descrito até que você o veja.

O que é red-green-refactor?

Red-green-refactor é o ciclo do TDD. Você escreve um teste que falha e descreve um comportamento (red) e, em seguida, escreve o mínimo de código necessário para fazê-lo passar (green). Com a suíte verde, você limpa o código sem mudar o que ele faz (refactor). Para uma revisão mais extensa com exemplos em React, veja TDD vs BDD in React.

Como funciona o fluxo de TDD com IA?

O TDD com IA funciona quando o humano escreve ou aprova os testes que falham e o agente recebe a instrução de fazê-los passar sem alterá-los. Os testes guardam os requisitos, e o agente fornece apenas a implementação. Os exemplos abaixo usam o Vitest 5.

Comece com um stub, para que o arquivo de teste importe algo real:

// src/cart/applyCoupon.ts
export type Coupon =
  | { code: string; kind: 'percent'; value: number }
  | { code: string; kind: 'fixed'; valueCents: number };

export function applyCoupon(
  subtotalCents: number,
  code: string,
  coupons: Coupon[],
): number {
  throw new Error('not implemented');
}

Depois, escreva a especificação:

// src/cart/applyCoupon.test.ts
import { describe, it, expect } from 'vitest';
import { applyCoupon, type Coupon } from './applyCoupon';

const coupons: Coupon[] = [
  { code: 'SAVE10', kind: 'percent', value: 10 },
  { code: 'FIVEOFF', kind: 'fixed', valueCents: 500 },
];

describe('applyCoupon', () => {
  it('takes a percentage off the subtotal', () => {
    expect(applyCoupon(2000, 'SAVE10', coupons)).toBe(1800);
  });

  it('takes a fixed amount off the subtotal', () => {
    expect(applyCoupon(2000, 'FIVEOFF', coupons)).toBe(1500);
  });

  it('never returns a negative total', () => {
    expect(applyCoupon(300, 'FIVEOFF', coupons)).toBe(0);
  });

  it('matches codes case-insensitively', () => {
    expect(applyCoupon(2000, 'save10', coupons)).toBe(1800);
  });

  it('rejects unknown codes', () => {
    expect(() => applyCoupon(2000, 'BOGUS', coupons)).toThrow('Unknown coupon');
  });
});

Como o stub existe e lança um erro, os cinco testes falham em tempo de execução, e não por causa de um import inexistente. Essa é uma fase red limpa. O último teste merece um olhar mais atento: o stub de fato lança um erro, mas a mensagem não corresponde a 'Unknown coupon', então o teste continua falhando pelo motivo certo. Cada teste fixa uma regra que o agente, de outra forma, teria de adivinhar: desconto percentual versus valor fixo, o piso em zero, o tratamento de maiúsculas e minúsculas.

Em seguida, entregue a tarefa com restrições explícitas:

Implement applyCoupon in src/cart/applyCoupon.ts so every test in
src/cart/applyCoupon.test.ts passes.

Rules:
- Do not create, edit, rename, skip or delete any *.test.ts file.
- Do not add .skip, .only, or mocks of applyCoupon.
- If a test looks wrong, stop and explain why instead of changing it.
- When finished, run `npx vitest run src/cart` and paste the full output.

A linha “stop and explain” oferece ao agente uma saída legítima que não seja alterar o teste. A saída colada poupa um passo, mas não prova nada. Mais hábitos de prompt que se encaixam nesse ciclo estão em 5 Tips and Tricks for AI-Assisted Coding.

Como o test-first dá errado com agentes?

Os agentes tendem a buscar o caminho mais curto até uma execução verde, e três desses caminhos ignoram completamente a especificação.

Editar ou excluir testes. Quando a implementação é difícil, mudar a expectativa é mais fácil do que atendê-la. Um toBe(1800) alterado, um .skip novo ou um arquivo de teste que sumiu produzem uma execução verde com o requisito removido.

Asserções fracas. Um teste escrito depois do código tende a confirmar o que o código faz, não o que ele deveria fazer:

// Confirms the code ran, not that it's right
it('applies coupon', () => {
  const result = applyCoupon(2000, 'SAVE10', coupons);
  expect(result).toBeDefined();
  expect(typeof result).toBe('number');
});

// Pins the behaviour
it('takes 10% off with SAVE10', () => {
  expect(applyCoupon(2000, 'SAVE10', coupons)).toBe(1800);
});

O teste fraco com toBeDefined() passa se applyCoupon retornar 0, 2000 ou 999999. É por isso que a ordem de escrita importa: testes que o agente escreve depois do próprio código tendem a se parecer com a primeira versão. Os agentes também costumam inflar as suítes com muitas verificações superficiais como essa, um padrão abordado em the smoke tests agents keep writing.

Mockar o que está sendo testado. Mockar o módulo sob teste significa que o teste exercita o mock, e esse teste continua verde independentemente do que a implementação real faça:

import { vi } from 'vitest';

vi.mock('./applyCoupon', () => ({ applyCoupon: () => 1800 }));
// Every assertion now tests the mock.

O Vitest faz o hoisting de vi.mock para cima dos imports, então a factory substitui o módulo antes de o código real ser carregado. O applyCoupon real nunca é executado. Faça mock das fronteiras (rede, armazenamento, tempo), não da unidade que você está especificando.

Quais mecanismos de proteção mantêm a especificação fixa?

Os mecanismos de proteção funcionam quando são mecânicos. As instruções reduzem a adulteração de testes, e o CI e a revisão pegam o que as instruções deixam passar.

Revise os diffs de teste separadamente. Leia as mudanças em *.test.ts como uma revisão à parte, antes da implementação. No GitHub, o CODEOWNERS com revisão obrigatória de code owner garante que uma pessoa designada aprove qualquer mudança em caminhos de teste.

Bloqueie ou proíba a edição de testes pelo agente.

Deixe o CI decidir o que é “pronto”. Rode npx vitest run no commit enviado. No CI, o Vitest faz a execução falhar se encontrar .only, a menos que você passe --allowOnly ou defina allowOnly: true. Ele não detecta .skip, então adicione uma verificação de diff nas branches de implementação:

changed=$(git diff --name-only origin/main...HEAD -- '*.test.ts' '*.spec.ts')
if [ -n "$changed" ]; then
  echo "Test files changed on an implementation branch:"
  echo "$changed"
  exit 1
fi

O checkout precisa ter histórico suficiente para que origin/main seja resolvido.

Verifique se as asserções testam comportamento. Para cada asserção, pergunte se uma implementação errada ainda conseguiria passar. Valores exatos, erros lançados e saídas visíveis sobrevivem a refatorações. Contagens de chamadas em helpers internos e verificações de tipo, não.

Mesmo uma suíte limpa tem um limite. Testes unitários passando mostram que o código trata os casos que alguém registrou. Eles não mostram que um usuário real consegue concluir o fluxo no navegador. Replays de sessão de uma funcionalidade já entregue podem revelar o checkout quebrando enquanto o CI está verde.

Quando o TDD não vale a pena?

O test-first custa mais do que retorna quando não é possível declarar o resultado esperado de antemão.

SituaçãoPor que dispensar o test-first
Protótipos descartáveisO código serve para responder a uma pergunta e depois ser excluído
Ajustes visuais de UI (espaçamento, cor, layout)O resultado certo é avaliado olhando, então ainda não há nada a afirmar

Se um protótipo sobreviver, escreva os testes antes de o agente transformá-lo em código de produção.

Conclusão

Quando os agentes escrevem o código, a especificação é a parte que só você pode fornecer, e os testes escritos primeiro a tornam executável. Na sua próxima tarefa com um agente, escreva primeiro o arquivo Vitest com os testes falhando e dê ao agente a instrução de não editá-lo. Faça o merge somente depois que o CI passar e uma revisão separada do diff de testes for aprovada.

Perguntas frequentes

Um linter consegue detectar testes pulados que um agente adiciona?

Sim. O pacote @vitest/eslint-plugin tem a regra vitest/no-disabled-tests, que sinaliza test.skip, it.skip e describe.skip, e a regra vitest/no-focused-tests, que sinaliza .only. A configuração recomendada apenas emite avisos para testes desativados, então defina essa regra como 'error' e rode o ESLint no CI. Projetos com Jest contam com regras de mesmo nome no eslint-plugin-jest. O lint pode deixar passar chamadas com alias, então mantenha também a verificação de diff do git.

Um agente de IA pode escrever os testes que falham no lugar de um humano?

Sim, desde que escrever os testes e escrever a implementação sejam etapas separadas. Faça uma sessão do agente redigir os testes com base no stub, revise você mesmo as asserções e faça o commit delas antes de a implementação começar. A sessão de implementação só deve ver testes que ela não tem permissão para alterar. Se a mesma sessão escrever os dois, os testes acabam descrevendo o próprio código dela, e não os requisitos.

Uma especificação escrita detalhada é suficiente sem escrever os testes primeiro?

Não. Uma especificação em texto ou um stub com comentários de documentação diz ao agente o que construir, mas nada confere o resultado contra ela. Um teste que falha não pode ser satisfeito por uma implementação que apenas parece plausível: ou ele passa, ou não passa. Use os dois. Coloque os requisitos e as restrições no prompt ou no arquivo de instruções e codifique como testes, executados pelo CI a cada commit, os casos que precisam ser atendidos.

O que fazer quando o agente diz que um teste está errado?

Analise você mesmo a explicação do agente e, se o teste estiver realmente errado, corrija-o fora da branch de implementação. Leve o teste corrigido para a main em um commit ou pull request próprio, revisado via CODEOWNERS se você o utiliza, e depois faça o rebase da branch de implementação. A verificação de diff compara a branch com origin/main, então uma correção de teste que já está na main não a dispara, e o agente nunca edita a especificação.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.