Logotipo do ConstructiCat
CodeBust.
Explorar a seção ▾

Testando Detalhes de Implementação.

Um teste faz asserções sobre como o código funciona internamente — campos privados, chamadas a métodos internos, estrutura do DOM ou classes CSS — em vez do comportamento observável do qual um consumidor real depende.

##Sinais e sintomas

Você reconhece este sintoma quando um teste vai além do contrato público e fixa a maquinaria por trás dele. Sinais comuns:

  • Asserções sobre estado/campos internos ou privados, ou invocação direta de métodos privados (muitas vezes via casts, reflexão ou // @ts-expect-error).
  • Espionar ou afirmar que um helper interno foi chamado (expect(internalCalc).toHaveBeenCalled()) em vez de verificar o resultado.
  • Testes de UI que consultam por classe CSS, tag, hooks data-* ou posição no DOM em vez de por role/label/texto — por exemplo container.querySelector('.btn-primary > span:nth-child(2)'), wrapper.state(), wrapper.find('SomeChildComponent').props().
  • Snapshots de objetos internos serializados — instâncias de componentes, entranhas da store, estruturas de dados privadas — em vez do que o usuário vê. (Um snapshot gigante de toda a saída renderizada é outro problema: o Teste Superespecificado.)
  • Testes que quebram a cada refatoração mesmo que a funcionalidade continue funcionando (falsos negativos) e, inversamente, continuam passando após um bug de lógica porque apenas reverificam a ligação interna (falsos positivos).
// SMELL: acopla o teste aos internos do componente e à estrutura do DOM
test('counter increments', () => {
  const wrapper = mount(<Counter />);
  wrapper.instance().handleClick();          // chama um método privado diretamente
  expect(wrapper.state('count')).toBe(1);    // faz asserção sobre estado interno
  expect(wrapper.find('.count-display').text()).toBe('1'); // seletor CSS frágil
});

O mesmo padrão aparece no lado servidor: expect(service._cache.size).toBe(1) faz asserção sobre um cache privado em vez do que o chamador recebe.

Smells relacionados: verificar mais do que o comportamento exige — número e ordem exatos de chamadas, matchers precisos demais, snapshots gigantes — é o Teste Superespecificado; um teste que substitui cada colaborador por um mock é Excessive Mocking. Este smell trata de onde a asserção olha: para as entranhas, como a estrutura do DOM ou o estado privado, em vez do que um usuário ou chamador consegue observar.

##Causas do problema

Por que acontece

  • O estado interno é fácil de alcançar — um campo público, um helper exportado ou container.querySelector está logo ali, enquanto exercitar o comportamento real exige mais preparação.
  • Perseguir métricas de cobertura: testar cada método privado 1:1 dá a sensação de minuciosidade.
  • O uso excessivo de mocks empurra as pessoas a afirmar "este colaborador foi chamado" em vez de "a coisa certa aconteceu."
  • Ferramentas que incentivam isso: shallow rendering / APIs instance() / state(), ou capturar nós pelo nome da classe.

Por que é prejudicial

  • Manutenibilidade / fragilidade. Este é o Teste Frágil de Meszaros, causado por Software Superespecificado: o teste fixa um comportamento que o consumidor nunca exigiu, de modo que refatorações inofensivas (renomear um método, reestruturar a marcação, alterar um campo privado) quebram testes que estavam verdes sem motivo real. Os testes se tornam um imposto sobre a refatoração em vez de uma rede de segurança.
  • Falsa confiança (o perigo central, segundo Kent C. Dodds). Testes de detalhes de implementação falham nas duas direções erradas: falsos negativos (o teste fica vermelho embora a funcionalidade ainda funcione) e falsos positivos (o teste permanece verde embora a funcionalidade esteja quebrada — você verificou a ligação interna, não o resultado). De qualquer forma, a suíte deixa de lhe dizer a verdade.
  • Legibilidade. O teste documenta como o código é construído, não o que ele garante. Quem lê não consegue identificar qual comportamento realmente importa, e o teste deixa de servir também como exemplo de uso ou especificação.
  • Acoplamento. Ele fixa as decisões de design atuais, desencorajando exatamente as refatorações que os testes deveriam tornar seguras.

##Tratamento

Teste através do contrato público — a mesma superfície que um chamador ou usuário real toca — e faça asserções sobre a saída observável: valores de retorno, erros lançados, eventos emitidos, estado persistido ou UI renderizada/visível.

  1. Identifique o consumidor. Para um módulo, é sua API exportada; para um componente de UI, é o usuário (cliques, digitação) e o que ele consegue ver.
  2. Forneça as entradas como um consumidor faria, e não chamando métodos privados. Dispare um clique real em vez de invocar o handler; chame o método público em vez do helper.
  3. Faça asserções sobre resultados, não sobre internos. Substitua verificações de state()/campo privado/toHaveBeenCalled por verificações sobre o que sai.
  4. Consulte a UI por acessibilidade, não por estrutura — role, label, texto — em vez de classes CSS, tags ou nth-child.
  5. Pare de testar métodos privados diretamente. Cubra-os através do método público que os utiliza; se uma unidade privada for complexa o suficiente para precisar de seus próprios testes, isso é um sinal de que ela deve ser extraída para seu próprio módulo com sua própria API pública.
  6. Reserve asserções de mock/spy para fronteiras verdadeiras (rede, tempo, gateway de pagamento) onde a própria chamada é o comportamento observável — não para colaboradores internos. Veja também Excessive Mocking.
// ANTES: testa detalhes de implementação
const wrapper = mount(<Counter />);
wrapper.instance().handleClick();
expect(wrapper.state('count')).toBe(1);
expect(wrapper.find('.count-display').text()).toBe('1');

// DEPOIS: testa o comportamento observável via contrato público voltado ao usuário
render(<Counter />);
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText('1')).toBeInTheDocument();

Regra prática: se uma refatoração que preserva o comportamento quebra o teste, o teste estava verificando um detalhe de implementação.

##Detectado por

  • eslint-testing-library testing-library/no-node-access — Proíbe o acesso direto a nós do DOM (querySelector, parentElement, children…) em vez de consultas voltadas ao usuário
  • eslint-testing-library testing-library/no-container — Proíbe consultar elementos por meio do container retornado por render