ConstructiCat Logo
CodeBust.
Browse section ▾

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.

##Signs and Symptoms

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 OrderService sem 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.env ou 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.
  • 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.
  • Você altera o código de produção só para testá-lo. Afrouxar private para public, adicionar hooks de teste if (testMode) { ... } ou criar subclasses apenas para alcançar os detalhes internos.
// 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.

##Reasons for the Problem

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.
  • 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.

##Treatment

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).

  1. 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.
  2. 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.
  3. 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.
  4. Extraia funções puras. Separe a computação dos efeitos colaterais; verifique os valores retornados, faça IO apenas na fronteira.
  5. 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.
  6. 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.
  7. 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 });

##Detected by