Apenas Para Testadores.
O código de produção contém métodos, acessadores de estado ou seams (costuras) que existem apenas para serem usados pelos testes, poluindo a API real e convidando a testes que verificam detalhes internos em vez do comportamento.
##Signs and Symptoms
Você encontra membros no código de produção cujos únicos chamadores vivem em arquivos de teste. Sinais reveladores:
- Métodos, getters/setters, exports ou parâmetros de construtor usados exclusivamente a partir de
*.test.ts/*.spec.ts. - Nomes ou marcadores com cara de teste:
getStateForTest,resetForTesting,__getInternal,FTO_*,forTest, comentários como// only used by tests, ou anotações como@VisibleForTesting/@TestOnly/@internal. - Visibilidade relaxada (um campo tornado
public/exportado, um#privatetransformado emprotected) puramente para que um teste consiga alcançar o estado interno. - "Seams" extras — um
setClock(...),setRandom(...)oureset()— adicionados apenas porque um teste precisou deles, não porque o design real os exija.
// payment-service.ts (código de PRODUÇÃO)
export class PaymentService {
#ledger: Entry[] = [];
charge(amount: number) { /* ... */ }
// Nada em produção jamais chama estes — apenas os testes chamam:
getLedgerForTest() { return this.#ledger; } // expõe detalhes internos
setClockForTest(now: () => Date) { this.now = now; } // seam exclusivo para testes
FTO_reset() { this.#ledger = []; } // "For Tests Only"
}
Uma verificação rápida: grep -rn 'forTest\|ForTesting\|FTO_' src/ que retorne ocorrências no código-fonte de produção, ou uma varredura de código morto (por exemplo, Knip em modo de produção) que reporte um export como não utilizado enquanto um teste claramente o importa.
##Reasons for the Problem
Por que acontece
- Adaptar testes a código não testável. Quando o código legado não foi projetado para ser testável, a forma mais rápida de fazer asserções sobre um resultado é abrir um buraco no SUT e ler suas entranhas — Meszaros aponta isso como a principal causa do For Tests Only.
- APIs assimétricas. Os clientes reais usam um objeto de uma forma (escrita); os testes o usam de forma simétrica (escrever e então ler de volta para verificar), de modo que os testadores "precisam" de acessadores que nenhum chamador de produção precisa.
- Pressão de prazo. Adicionar uma porta dos fundos é mais barato no momento do que refatorar para um design no qual o comportamento é observável por meio do contrato público.
Por que é prejudicial
- Legibilidade. A API pública deixa de dizer a verdade — os mantenedores não conseguem distinguir o contrato real do andaime de teste, e cada leitor tem que se perguntar "esse método é realmente usado?"
- Encapsulamento e confiabilidade. O estado interno se torna alcançável e mutável no código que vai para produção. Uma chamada perdida a
FTO_reset()ou um setter vazado pode corromper o estado em produção; a superfície extra também incha o bundle e amplia a superfície de ataque. - Falsa confiança. Testes que cutucam o estado privado fazem asserções sobre a implementação, não sobre o comportamento. Eles podem continuar verdes enquanto o contrato público está quebrado, e quebram em refatorações inofensivas — testes frágeis que testam a coisa errada.
- Manutenibilidade. Os membros exclusivos para testes parecem código morto, mas não podem ser excluídos; cada mudança precisa levar em conta chamadores fantasmas, e o smell tende a se multiplicar à medida que mais testes reutilizam a porta dos fundos.
##Treatment
Trate a porta dos fundos como um sinal de design, não como um detalhe de fixture.
- Teste primeiro por meio do comportamento observável. Faça asserções sobre valores de retorno, eventos emitidos, saída persistida ou interações com colaboradores (via test doubles) em vez de alcançar os detalhes internos. A maioria dos acessadores
getXForTestdesaparece assim que você verifica o que o objeto faz, e não o que ele guarda. - Use uma Test-Specific Subclass quando você genuinamente precisar de acesso interno. Estenda a classe no teste para expor um membro
protected, em vez de ampliar a visibilidade em produção. - Faça das seams parte do design real, não escotilhas exclusivas para testes. Injetar um relógio ou um RNG por meio do construtor normal é injeção de dependência legítima; um mutador
setClockForTest()é um smell. Se uma seam só faz sentido para os testes, empurre o comportamento para um Strategy/Null Object que a produção instala por padrão e que o teste troca. - Se a exposição for realmente inevitável, rotule-a de forma evidente e isole-a. Marque-a com
@VisibleForTesting/@internal(ou uma convenção de nomenclaturaFTO_) e garanta que o código de produção nunca a chame (veja os detectores). Uma seam marcada e protegida é melhor do que uma silenciosa. - Cace os infratores existentes. Faça
grepporforTest/ForTesting/FTO_e rode uma ferramenta de uso/código morto, como o Knip em modo de produção, para revelar exports referenciados apenas por arquivos de teste.
// ANTES — a produção carrega um acessador exclusivo para testes
export class Cart {
#items: Item[] = [];
add(i: Item) { this.#items.push(i); }
getItemsForTest() { return this.#items; } // apenas para testadores
}
// teste
expect(cart.getItemsForTest()).toHaveLength(1);
// DEPOIS — verifique o comportamento por meio do contrato real
export class Cart {
#items: Item[] = [];
add(i: Item) { this.#items.push(i); }
get count() { return this.#items.length; }
get total() { return this.#items.reduce((s, i) => s + i.price, 0); }
}
// teste
cart.add({ price: 10 });
expect(cart.count).toBe(1);
expect(cart.total).toBe(10);
Se você ainda precisar de acesso interno, restrinja a seam aos testes com uma subclasse, em vez de ao mundo inteiro:
// a produção permanece limpa: queue é protected, não public
export class Scheduler {
protected queue: Job[] = [];
enqueue(j: Job) { this.queue.push(j); }
}
// apenas no arquivo de teste
class TestScheduler extends Scheduler {
peek() { return this.queue; }
}
##Detected by
- sonar java:S5803 — Membros "@VisibleForTesting" não devem ser acessados a partir do código de produção