Programação Por Equipe DevBras

Capa do artigo: Fixture gigante esconde a regra: monte só o dado que o teste usa

Fixture gigante esconde a regra: monte só o dado que o teste usa

Troque o JSON de cem linhas por um construtor de dados com padrões sensatos e deixe no teste só o campo que decide o resultado.

O teste do frete grátis quebrou porque alguém mudou o formato do CEP no arquivo pedido-completo.json. O arquivo tem 140 linhas: cliente, endereço, três itens, cupom, histórico de status. O teste só afirma que pedido acima de R$ 200 não paga frete. A resposta curta: o teste deve mostrar, no próprio corpo, só o dado que decide o resultado. Use uma função construtora com valores padrão válidos e sobrescreva apenas o campo que importa para aquela regra. O resto existe, mas não aparece.

Quem lê o teste depois entende a regra sem abrir outro arquivo. E quando um campo irrelevante muda, o teste não quebra por isso.

Por que o fixture grande vira um problema

O fixture começa pequeno e honesto. Depois um teste precisa de cupom, e alguém adiciona o cupom no arquivo compartilhado. Outro precisa de endereço no Nordeste, e o endereço muda. Em um ano, o arquivo atende vinte testes e ninguém sabe qual campo serve a qual teste.

Isso causa três estragos. Primeiro, o teste deixa de documentar a regra: para saber por que o frete deu zero, você precisa somar os itens do JSON de cabeça. Segundo, qualquer mudança no arquivo pode quebrar testes que não têm nada a ver com ela, e as pessoas passam a ter medo de mexer. Terceiro, o fixture esconde acoplamento: um teste passa por acaso porque o pedido compartilhado tem um cupom que ele nem sabia que existia.

A pergunta para o teste é simples: se eu mudar este valor, o resultado muda? Se sim, ele deve aparecer no teste. Se não, ele deve ficar escondido num padrão.

Como trocar o fixture por um construtor de dados

A ideia tem nomes conhecidos. O Object Mother é um lugar com funções que fabricam objetos de teste prontos. O Test Data Builder vai um passo além: começa de um objeto válido e permite sobrescrever só o que interessa. Na prática, uma função com padrões e um parâmetro de sobrescrita já resolve a maior parte dos casos.

  1. Liste as regras que o arquivo de fixture atende hoje e quais campos cada teste realmente usa.
  2. Crie uma função construtora por entidade, com valores padrão que formam um objeto válido e sem graça.
  3. Permita sobrescrever qualquer campo com um objeto de opções.
  4. Reescreva um teste por vez, deixando no corpo só os campos que decidem o resultado.
  5. Dê ao teste um nome que diga a regra, não o dado.
  6. Apague do fixture compartilhado os campos que ninguém mais usa, e depois o arquivo, se ele ficar vazio.

O construtor do pedido

No exemplo da loja, os valores ficam em centavos, como explicado em dinheiro em centavos. O construtor monta um pedido válido e permite trocar qualquer parte.

// test/construtores.js
function umItem(opcoes = {}) {
  return {
    sku: 'CAMISETA-P',
    quantidade: 1,
    precoCentavos: 5000,
    ...opcoes,
  };
}

function umPedido(opcoes = {}) {
  return {
    id: 'pedido-teste',
    cliente: { nome: 'Cliente Teste', email: 'cliente@exemplo.com' },
    endereco: { cep: '01001-000', uf: 'SP' },
    itens: [umItem()],
    cupom: null,
    ...opcoes,
  };
}

module.exports = { umItem, umPedido };

Os padrões são deliberadamente neutros: um item, sem cupom, endereço em SP. Nada ali deveria disparar regra especial. Se um padrão começar a disparar regra, troque o padrão, não os testes.

Os testes que contam a regra

// frete.test.js
const test = require('node:test');
const assert = require('node:assert/strict');
const { umPedido, umItem } = require('./test/construtores');
const { calcularFreteCentavos } = require('../src/frete');

test('pedido a partir de R$ 200 tem frete grátis', () => {
  const pedido = umPedido({
    itens: [umItem({ precoCentavos: 12000 }), umItem({ precoCentavos: 8000 })],
  });
  assert.equal(calcularFreteCentavos(pedido), 0);
});

test('pedido de R$ 199,99 ainda paga frete', () => {
  const pedido = umPedido({
    itens: [umItem({ precoCentavos: 19999 })],
  });
  assert.ok(calcularFreteCentavos(pedido) > 0);
});

test('frete grátis olha o valor depois do cupom', () => {
  const pedido = umPedido({
    itens: [umItem({ precoCentavos: 21000 })],
    cupom: { descontoCentavos: 2000 },
  });
  assert.ok(calcularFreteCentavos(pedido) > 0);
});

Compare com a versão antiga, que carregava pedido-completo.json e afirmava 0. Lá, para entender por que o frete era zero, você abria o arquivo, achava três itens de 50, 80 e 90 reais e somava. Aqui, 120 mais 80 dá 200 e está escrito no teste. O caso de R$ 199,99 marca a fronteira de um centavo, que o fixture nunca cobriu porque ninguém queria criar outro arquivo de 140 linhas.

O terceiro teste mostra outra vantagem: a regra do cupom ficou visível. Antes, o pedido compartilhado tinha um cupom que ninguém lembrava, e o teste do frete passava ou falhava dependendo de como a soma tratava o desconto.

Quando o objeto tem relação com banco

Se o teste precisa gravar no banco, o mesmo construtor serve: ele monta o objeto e uma função auxiliar grava. Gere identificadores únicos em vez de fixos, para dois testes não disputarem a mesma linha. O post sobre teste que só passa sozinho mostra por que o "cliente 1" compartilhado é fonte de falha intermitente.

Quando um arquivo de dados ainda faz sentido

Fixture em arquivo não é proibido. Ele funciona bem para dados que são estáveis e grandes por natureza: uma resposta real de API gravada para teste de contrato, um XML de nota fiscal de exemplo, uma tabela de alíquotas oficial. Nesses casos, o arquivo é o próprio objeto do teste, e mudar o arquivo deve mesmo quebrar o teste.

A diferença é o propósito. Um arquivo que representa algo externo e real tem dono e motivo para existir. Um arquivo que é só "o pedido de teste de todo mundo" vira um segundo sistema, sem dono, que cresce a cada teste novo. O caso da resposta gravada de fornecedor aparece no post sobre mock que não parece o serviço real.

Erros comuns

  • Padrões que disparam regra. Se o pedido padrão já tem cupom ou endereço com frete especial, todo teste herda esse comportamento sem perceber.
  • Sobrescrever campos que não importam "para garantir". O teste volta a ficar poluído e o leitor não sabe qual campo é o essencial.
  • Nomear casos como "dados1" ou "pedidoValido2". O nome não diz a regra, e o próximo leitor precisa abrir o objeto para entender.
  • Compartilhar e mutar o mesmo objeto entre testes. Um teste altera o pedido e o seguinte recebe a versão alterada; o construtor deve devolver um objeto novo a cada chamada.
  • Copiar o fixture inteiro para cada teste. Troca um arquivo gigante por vinte cópias gigantes, com o mesmo problema multiplicado.

Checklist

  • Cada teste mostra no corpo os valores que decidem o resultado.
  • Existe um construtor por entidade principal, com padrões válidos e neutros.
  • O construtor devolve um objeto novo a cada chamada.
  • Os nomes dos testes descrevem a regra de negócio.
  • Há pelo menos um caso na fronteira de cada limite numérico.
  • Arquivos de dados que sobraram representam algo externo e têm dono.
  • Mudar um campo irrelevante do padrão não quebra nenhum teste.

Abra o maior fixture da sua suíte e escolha um teste que o usa: quantos campos daquele arquivo você consegue apagar sem que esse teste mude de resultado?

Avalie este artigo

Referência: https://martinfowler.com/bliki/ObjectMother.html