Código Difícil de Testar.
Código de produção cujo design (acoplamento forte, dependências ocultas, estado global, IO não determinística ou interfaces apenas assíncronas) força os testes a contorções estranhas, ou torna impossível exercitar uma unidade isoladamente.
##Sinais e sintomas
Este smell vive no código de produção, mas você o descobre ao escrever testes. O sinal revelador é que um teste unitário não pode ser escrito de forma limpa — o teste precisa lutar contra o design para colocar o código sob teste em um estado conhecido e observar o resultado.
Fique atento a estes sintomas:
- Blocos de arrange gigantes. Você precisa construir uma teia de colaboradores só para instanciar a classe sob teste ("não consigo testar
OrderServicesem também montar um banco de dados, um gateway e um carregador de configuração"). - Nenhuma costura para injetar um dublê. O código chama
new ConcreteThing(), acessa um singleton estático (Database.getInstance()) ou usa a rede/sistema de arquivos/relógio diretamente, então não há onde substituir por um stub ou fake. - Não determinismo embutido. A lógica depende de
Date.now(),Math.random(),process.envou do tempo do relógio que o teste não pode controlar, produzindo resultados instáveis ou irrepetíveis. - Sem ponto de observação / sem ponto de controle. O resultado que você quer verificar está enterrado em estado privado ou em um efeito colateral, então os testes acessam via reflexão, casts ou getters exclusivos de teste (veja Apenas Para Testadores).
- Interface apenas assíncrona. A única forma de acionar o código é iniciar um timer/thread/fila e então usar
sleep()ou polling, porque a conclusão nunca é diretamente observável (veja Sleepy Test). - Você altera o código de produção só para testá-lo. Afrouxar
privateparapublic, adicionar hooks de testeif (testMode) { ... }ou criar subclasses apenas para alcançar os detalhes internos (veja Lógica de Teste em Produção).
// Difícil de testar: dependências ocultas + fixas, estado global, não determinismo
class OrderService {
placeOrder(cart) {
const db = Database.getInstance(); // singleton global (sem costura)
const gateway = new StripeGateway(API_KEY); // dependência concreta fixa -> rede real
const id = Math.random().toString(36).slice(2); // não determinístico
const charge = gateway.charge(cart.total); // chamada HTTP real dentro da unidade
db.save({ id, at: Date.now(), charge }); // relógio não injetado
return id;
}
}
// Para fazer "teste unitário" disso você precisa acessar o Stripe, fazer monkey-patch global de Date/Math,
// e inspecionar um singleton compartilhado — ou seja, Teste Indireto por meio de interfaces estranhas.
##Causas do problema
Por que isso acontece
- Test-Last em código legado. Meszaros nomeia a causa raiz como falta de Design para Testabilidade. A testabilidade surge naturalmente do TDD, mas quando os testes são escritos "por último," nada pressionou o design a expor pontos de controle e observação, então ela precisa ser adaptada depois.
- Acoplamento forte e dependências fixas no código. Instanciar colaboradores concretos com
new(ou obtê-los de singletons estáticos) significa que você não consegue substituí-los por dublês de teste — o teste exercita a unidade e tudo o que ela toca. - Estado global/compartilhado e entradas ocultas. Singletons, estáticos, tempo ambiente, aleatoriedade e leituras do ambiente são entradas que o teste nunca declarou e não pode definir, então o comportamento é implícito e incontrolável.
- Assincronia e efeitos colaterais no núcleo. Quando a lógica de negócio está emaranhada com threads, filas, IO ou a interface de usuário, não há um valor puro para verificar.
Por que isso é prejudicial
- Confiabilidade. Testes forçados a usar IO real, sleeps, singletons compartilhados ou relógios globais ficam lentos, instáveis e dependentes de ordem — o caminho clássico para um Teste Errático/Frágil (veja Teste Errático).
- Manutenibilidade. Configurações enormes e testes que alcançam os detalhes internos acoplam a suíte de testes aos detalhes de implementação, então refatorações inofensivas quebram testes (Teste Frágil / Fixture Frágil).
- Falsa confiança. Os caminhos de código mais difíceis e importantes são testados apenas por interfaces grosseiras e indiretas — ou são totalmente ignorados. Os números de cobertura parecem bons enquanto a lógica arriscada mal é exercitada.
- Legibilidade. Um teste dominado por andaimes obscurece o único comportamento que ele deveria especificar, então não documenta nada.
No Open Catalog of Test Smells, o Código Difícil de Testar é a contraparte, do lado da produção, de smells de teste como Teste Indireto e Teste Frágil: o design ruim é a causa, os testes estranhos são o sintoma.
##Tratamento
Corrija o design, não o teste. O objetivo é dar a cada unidade um ponto de controle (uma forma de colocá-la em um estado conhecido) e um ponto de observação (uma forma de ler o resultado).
- Introduza costuras via Injeção de Dependência. Passe os colaboradores (injeção via construtor) em vez de construí-los ou procurá-los. Injete também as entradas ambientes — relógio, gerador de id/uuid, fonte de aleatoriedade — para que se tornem parâmetros controláveis.
- Dependa de abstrações. Programe para uma interface e substitua por um stub/fake/mock nos testes. Isso remove diretamente a Hard-wired Dependency do Designite e reduz a Excessive Dependency.
- Aplique o padrão Humble Object. Empurre as partes genuinamente difíceis de testar (UI, cola assíncrona, IO bruta) para um adaptador fino e sem lógica, e mova a lógica de decisão para um objeto simples e síncrono que você possa testar diretamente.
- Extraia funções puras. Separe a computação dos efeitos colaterais; verifique os valores retornados, faça IO apenas na fronteira.
- Torne o assíncrono observável. Retorne uma promise/future ou exponha um sinal de conclusão, e injete o escalonador para que os testes usem fake timers em vez de
sleep. - Para código legado que você ainda não pode reprojetar, use uma Subclasse Específica de Teste ou uma costura de escopo restrito (técnicas de Feathers) para colocar o código sob teste, e então refatore em direção à injeção — em vez de deixar hooks
if (testMode)permanentes em produção. - Evite estado global/estático. Substitua singletons por dependências passadas explicitamente para que os testes não compartilhem nem precisem redefinir estado oculto.
// DEPOIS: dependências, relógio e id são injetados -> testável isoladamente
class OrderService {
constructor(db, gateway, clock = () => Date.now(), newId = uuid) {
this.db = db; this.gateway = gateway; this.clock = clock; this.newId = newId;
}
placeOrder(cart) {
const id = this.newId();
const charge = this.gateway.charge(cart.total); // uma abstração PaymentGateway
this.db.save({ id, at: this.clock(), charge });
return id;
}
}
// Teste: determinístico, sem rede, sem patch global, rápido.
const svc = new OrderService(fakeDb, fakeGateway, () => 1000, () => 'id-1');
expect(svc.placeOrder({ total: 50 })).toBe('id-1');
expect(fakeGateway.charge).toHaveBeenCalledWith(50);
expect(fakeDb.saved[0]).toEqual({ id: 'id-1', at: 1000, charge: fakeGateway.result });
##Detectado por
- designite Hard-wired Dependency — Dependência fixa (problema de testabilidade)
- designite Excessive Dependency — Dependência excessiva (cheiro de testabilidade)
- designite Global State — Estado Global (cheiro de testabilidade)
- designite Law of Demeter Violation — Violação da Lei de Deméter (cheiro de testabilidade)