af aprenda frontend
módulo 07 qualidade

O que testar — e o que não.

Foco em comportamento, não implementação. Sinais de teste frágil. Cobertura como guia, não como meta.

Escrever testes não é uma obrigação a ser cumprida mecanicamente. É uma ferramenta que serve a um objetivo: confiança para mudar código. Quando você escreve testes que testam a implementação em vez do comportamento, ou que quebram a cada refactor, a ferramenta trabalha contra você. Esta lição é sobre como distinguir testes que valem o investimento dos que não valem.

Testar comportamento, não implementação

A distinção mais importante em testes:

Teste de implementação: verifica como o código funciona internamente — nomes de variáveis, estrutura de componentes, quais hooks são usados, em que ordem funções são chamadas.

Teste de comportamento: verifica o que o sistema faz do ponto de vista de quem o usa — o usuário clica no botão e o contador incrementa.

tsx
// ❌ teste de implementação — verifica o estado interno
it("useState é chamado com false inicialmente", () => {
  const useStateSpy = vi.spyOn(React, "useState");
  render(<LikeButton curtido={false} onCurtir={() => {}} contagem={5} />);
  expect(useStateSpy).toHaveBeenCalledWith(false);
});
// Se o LikeButton for refatorado para não usar useState (estado vem só de props),
// este teste quebra — mas o comportamento público é idêntico

// ✅ teste de comportamento — verifica o que o usuário vê
it("exibe ❤️ quando curtido e 🤍 quando não curtido", () => {
  const { rerender } = render(
    <LikeButton curtido={false} onCurtir={() => {}} contagem={5} />
  );
  expect(screen.getByRole("button")).toHaveTextContent("🤍");

  rerender(<LikeButton curtido={true} onCurtir={() => {}} contagem={6} />);
  expect(screen.getByRole("button")).toHaveTextContent("❤️");
});
// Se a implementação interna mudar, este teste continua válido
Implementação vs. comportamento — o mesmo cenário, dois enfoques.

A regra prática: se renomear uma variável de estado ou extrair uma função auxiliar quebrar o teste, o teste está testando implementação.

Sinais de teste frágil

Um teste frágil quebra com frequência por razões que não indicam um bug real:

Sinal 1 — seletores de CSS interno. Testes que encontram elementos por classes CSS internas quebram quando você refatora o estilo:

tsx
// ❌ frágil — quebra se você renomear a classe
screen.getByTestId("botao-curtir");
document.querySelector(".botao-curtir--ativo");

// ✅ robusto — o papel e o nome acessível raramente mudam
screen.getByRole("button", { name: /curtir/i });
Seletor CSS vs. papel de acessibilidade.

Sinal 2 — verificar detalhes de renderização irrelevantes. Testar que o HTML tem uma estrutura específica de <div> dentro de <span> dentro de <article> — em vez de que o conteúdo certo aparece no lugar certo.

Sinal 3 — muitos mocks. Quando um teste substitui tantas dependências que pouca coisa real está sendo testada:

ts
// ❌ tudo está mockado — o teste verifica apenas que os mocks são chamados
vi.mock("./useArtigos");
vi.mock("./filtrarArtigos");
vi.mock("./ArticleCard");

it("HomePage chama useArtigos e passa artigos para ArticleList", () => {
  vi.mocked(useArtigos).mockReturnValue({ artigos: [], carregando: false, erro: null });
  render(<HomePage />);
  expect(useArtigos).toHaveBeenCalled(); // teste do mock, não do código
});
Excesso de mocks — o que está sendo testado aqui?

Sinal 4 — testes que dependem de ordem. Se rodar os testes em ordem diferente muda os resultados, há vazamento de estado entre testes — geralmente localStorage ou variáveis globais não limpos no beforeEach.

O que não precisa de teste

Nem tudo precisa de teste automatizado. O critério: o benefício (confiança, documentação, detecção de regressão) justifica o custo (escrever, manter)?

ts
// configuração de ferramentas — não tem lógica de negócio
// vitest.config.ts, vite.config.ts, tsconfig.json

// tipos TypeScript sem lógica
type Artigo = { id: number; titulo: string; }; // o TypeScript já valida

// código gerado automaticamente (build output, mocks gerados)

// wrappers triviais
function log(msg: string) { console.log(msg); }
// o teste seria: expect(console.log).toHaveBeenCalledWith("...")
// mais complexo que o código

// componentes de apresentação sem lógica
function Divider() { return <hr className="divisor" />; }
// teste seria: expect(getByRole("separator")).toBeInTheDocument()
// valor mínimo — o componente não tem lógica
Casos que geralmente não precisam de teste.

O que precisa de teste:

  • Funções puras com lógica não trivial (formatação, cálculo, filtro)
  • Componentes com comportamento (interação, estado, condicional)
  • Integração entre componente e efeito/fetch/localStorage
  • Fluxos críticos do produto (E2E)

Cobertura como guia, não meta

O relatório de cobertura mostra quais linhas foram executadas ao menos uma vez durante os testes. É uma ferramenta útil para identificar código não testado — não para garantir que o código funciona:

bash
npm run coverage
# ou
npx vitest run --coverage
Gerar relatório de cobertura.

Output típico:

plaintext
File            | % Stmts | % Branch | % Funcs | % Lines
----------------|---------|----------|---------|--------
utils.ts        |   94.23 |    87.50 |  100.00 |   94.23
ArticleCard.tsx |   78.12 |    66.66 |   83.33 |   78.12
LikeButton.tsx  |  100.00 |   100.00 |  100.00 |  100.00

O que os números dizem:

  • ArticleCard.tsx com 78% de linhas cobertas → há cenários não testados. Vale investigar quais.
  • LikeButton.tsx com 100% → todas as linhas foram executadas. Não garante que todos os cenários foram verificados — só que todas as linhas rodaram.

O número que importa: os casos críticos do produto têm testes? Se o fluxo de curtir está testado e o filtro de tag está testado, 78% de cobertura no ArticleCard pode ser aceitável. Se a função de cálculo de tempo de leitura não tem nenhum teste, 100% de cobertura em outros arquivos não ajuda.

Refatorar um teste frágil

tsx
// ❌ frágil — dependente de estrutura HTML interna e data-testid
it("exibe o badge 'Novo' para artigos recentes", () => {
  render(<ArticleCard artigo={artigoRecente} curtido={false} onCurtir={() => {}} />);

  const badge = document.querySelector("[data-testid='badge-novo']");
  expect(badge).not.toBeNull();
  expect(badge?.classList.contains("badge-novo")).toBe(true);
});

// ✅ robusto — baseado no texto visível que o usuário vê
it("exibe o badge 'Novo' para artigos publicados nos últimos 7 dias", () => {
  const artigoRecente = {
    ...artigoFixture,
    publicadoEm: new Date().toISOString(), // hoje
  };

  render(<ArticleCard artigo={artigoRecente} curtido={false} onCurtir={() => {}} />);

  expect(screen.getByText("Novo")).toBeInTheDocument();
});

it("não exibe o badge 'Novo' para artigos com mais de 7 dias", () => {
  const artigoAntigo = {
    ...artigoFixture,
    publicadoEm: "2023-01-01T00:00:00Z",
  };

  render(<ArticleCard artigo={artigoAntigo} curtido={false} onCurtir={() => {}} />);

  expect(screen.queryByText("Novo")).not.toBeInTheDocument();
});
Refatoração — de frágil para robusto.

Resumo

  • Comportamento, não implementação: o teste não deve quebrar quando você renomeia variáveis, extrai funções ou reorganiza a implementação — apenas quando o comportamento público muda.
  • Testes frágeis: seletores CSS internos, verificação de estrutura HTML irrelevante, excesso de mocks, dependência de ordem de execução.
  • O que não testar: configuração de ferramentas, tipos TypeScript sem lógica, wrappers triviais, componentes de apresentação sem comportamento.
  • Cobertura como guia: use para identificar pontos não testados, não como meta. 100% de cobertura não é 100% correto.
  • A pergunta que guia cada decisão: o benefício de ter este teste justifica o custo de mantê-lo?
/ checkpoint verifique seu entendimento
questão 1 de 4

O que significa testar comportamento em vez de implementação?