---
id: testing-strategy-beyond-unit
domain: engineering
agents: [qa, dev]
when: "ao desenhar a estratégia de testes de uma feature ou avaliar a cobertura de uma story"
---

# Testing strategy beyond unit — testes que pegam bug, não que decoram cobertura

Suíte de teste medíocre tem uma "cara": ela é verde, tem 85% de cobertura, e mesmo assim
bugs escapam pra produção. O motivo é quase sempre o mesmo — os testes visitam o código
sem testar nada, ou estão tão acoplados aos internos que quebram a cada refactor e ninguém
mais confia neles. Este pack ensina a **escolher o tipo de teste certo, escrevê-lo pra pegar
bug de verdade, e medir se ele presta** — destilado do
[goldbergyoni/javascript-testing-best-practices](https://github.com/goldbergyoni/javascript-testing-best-practices)
(Yoni Goldberg). A regra-mãe (o "Golden Rule" do repo): **código de teste não é código de
produção — projete-o pra ser curto, raso e óbvio. Olhe um teste e entenda a intenção na hora.**
Tudo abaixo deriva disso.

## O problema / os tells

Se a suíte tem 3+ destes, ela é teatro de cobertura, não rede de segurança:

1. **Nome de teste que não diz nada** — `it("Add product")`, `it("Test addNewOrder")`. Falhou no
   CI: você não faz ideia do que quebrou sem abrir o código.
2. **Sem separação AAA** — arrange, act e assert grudados num bloco só; o leitor gasta CPU mental
   só pra achar onde o teste age e onde verifica.
3. **Asserção imperativa** — `forEach` + `if` + flags booleanas + `throw` manual em vez de um
   `expect` declarativo. A "história" do teste some no meio do controle de fluxo.
4. **Testa método privado / internals** — o teste chama `calculateVATAdd()` (interno) e quebra
   quando você renomeia o método, embora o resultado final esteja certo. O teste vira o menino
   que gritou lobo: dá falso-positivo até alguém ignorar o CI e um bug real passar.
5. **Mock que verifica "foi chamado com esses args"** — `mock.expects("deleteProduct").withArgs(...)`.
   Acoplado ao como, não ao quê. Qualquer refactor obriga a caçar e reescrever todos os mocks.
6. **Input `"Foo"` / `123`** — input sintético nunca dispara o regex de validação, o path de erro,
   o caso de borda. Verde no dev, vermelho quando um usuário manda `"@3e2ddsf . ##' 1 fdsfds"`.
7. **`setTimeout(2000)` esperando algo async** — o teste é lento (espera demais) ou flaky (espera
   de menos). Trade-off entre lentidão e flakiness, perde nos dois.
8. **Seed global de DB** (`before(() => DB.seed('seed.json'))`) — testes acoplados a dados que
   vivem fora deles; dois testes mutam a mesma linha e o deploy aborta sem causa óbvia.
9. **Selector por classe CSS** — `wrapper.find("[className='thin-border']")`. O designer troca a
   classe e o teste de *lógica* de login quebra.
10. **Snapshot gigante externo** — `toMatchSnapshot()` de 2000 linhas que ninguém leu; quebra a
    cada espaço, comentário ou mudança de CSS, e o autor aprova cegamente o "novo correto".
11. **100% de cobertura, 0% de teste** — o teste chama a função e não tem **nenhum** `expect`.
    Visitou todas as linhas, não verificou nada.
12. **Só pirâmide de unit** — 50% do budget em unit tests pra uma app que é integração-cêntrica
    (ingest Kafka → data warehouse → UI), quase sem lógica. Muito esforço, 20% de cobertura real.

## Os princípios do craft (com pares bad→good reais do repo)

### 1. Nome do teste em 3 partes: o quê / cenário / esperado
O relatório do CI tem que dizer o que está quebrado pra quem **não** conhece o código (o ops que
faz deploy, você daqui a dois anos). Três partes: (1) unidade sob teste, (2) cenário/circunstância,
(3) resultado esperado.

```javascript
// ❌ "Add product" — falhou no deploy. Isso te diz o que está com defeito?

// ✅ describe aninhado carrega a parte (1); o it() carrega (2) e (3)
describe('Products Service', () => {
  describe('Add new product', () => {
    it('When no price is specified, then the product status is pending approval', () => {
      const newProduct = new ProductService().add(/* ... */);
      expect(newProduct.status).to.equal('pendingApproval');
    });
  });
});
```

### 2. Estruture com AAA — Arrange, Act, Assert, separados
Três seções marcadas. Garante que o leitor não gasta "brain-CPU" decifrando o plano do teste.
Act e Assert costumam ser 1 linha cada.

```javascript
// ❌ tudo num bloco só, mais difícil de interpretar
test("Should be classified as premium", () => {
  const customerToClassify = { spent: 505, joined: new Date(), id: 1 };
  const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" });
  const received = customerClassifier.classifyCustomer(customerToClassify);
  expect(received).toMatch("premium");
});

// ✅ Arrange / Act / Assert separados
test("When customer spent more than 500$, should be classified as premium", () => {
  // Arrange
  const customerToClassify = { spent: 505, joined: new Date(), id: 1 };
  sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" });
  // Act
  const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);
  // Assert
  expect(receivedClassification).toMatch("premium");
});
```

### 3. Asserção declarativa (BDD), não fluxo imperativo
Código imperativo com `if`/`forEach`/flags força o leitor a executar o teste na cabeça. Use
`expect`/`should` declarativo. Se a asserção que você precisa não existe e é repetível, estenda o
matcher — não escreva lógica de verificação inline.

```javascript
// ❌ o leitor tem que percorrer o loop pra entender a história do teste
allAdmins.forEach(u => {
  if (u === "user1") assert.notEqual(u, "user1", "A user was found and not admin");
  if (u === "admin1") admin1Found = true;
  if (u === "admin2") admin2Found = true;
});
if (!admin1Found || !admin2Found) throw new Error("Not all admins were returned");

// ✅ uma linha declarativa
expect(allAdmins)
  .to.include.ordered.members(["admin1", "admin2"])
  .but.not.include.ordered.members(["user1"]);
```

Corolário — **não capture erro, espere-o.** Nada de `try/catch` + flag pra provar que lançou.

```javascript
// ❌ se a asserção final falhar, o relatório só diz "valor null" — nada sobre exceção faltando
let caught = null;
try { await addNewProduct({}); } catch (e) { expect(e.code).to.equal("InvalidInput"); caught = e; }
expect(caught).not.to.be.null;

// ✅ legível até pro QA/PM, e o tipo do erro é verificado
await expect(addNewProduct({}))
  .to.eventually.throw(AppError)
  .with.property("code", "InvalidInput");
```

### 4. Teste comportamento via API pública (black-box), nunca internals
Verificar o comportamento público testa a implementação privada **implicitamente**. Testar o
interno (white-box) desloca o foco pro detalhe e quebra o teste em refactors inofensivos.

```javascript
// ❌ white-box: testa um método interno que nem é requisito
class ProductService {
  calculateVATAdd(price) { return { finalPrice: price * 1.2 }; } // renomear → testes quebram
  getPrice(id) { return this.calculateVATAdd(DB.getProduct(id).price).finalPrice; }
}
expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0);

// ✅ teste o que é requisito (preço final via método público); o VAT é coberto de tabela
```

No frontend, a mesma regra vira "renderize de verdade e aja como o usuário", não shallow render +
invocar método interno:

```javascript
// ✅ realista: monta o componente, clica, verifica o que o usuário vê
const wrapper = mount(<Calendar showFilters={false} />);
wrapper.find("button").simulate("click");
expect(wrapper.text().includes("Choose Filter"));

// ❌ shallow + .instance().showFilters() — tapeia a UI e chama método privado (white-box)
```

### 5. Escolha o test double certo: stub/spy (comportamento), não mock (internals)
Antes de qualquer double, pergunte: **isto aparece (ou poderia aparecer) no documento de requisitos?**
Se não, é cheiro de white-box. Stub pra simular um cenário ("pagamento fora do ar"), spy pra
verificar um efeito que **é** requisito ("envie e-mail se o pagamento falhar"). Mock que checa
"foi chamado com esses tipos/args" testa o interno — vai mudar toda hora.

```javascript
// ❌ mock: o objetivo virou verificar a chamada interna ao DAL, não o requisito
const dataAccessMock = sinon.mock(DAL);
dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, product, true, false);
new ProductService().deletePrice(product);
dataAccessMock.verify();

// ✅ spy: verifica o requisito (e-mail enviado); tocar no interno é só efeito colateral
const spy = sinon.spy(Emailer.prototype, "sendEmail");
new ProductService().deletePrice(product);
expect(spy.calledOnce).to.be.true;
```

### 6. Input realista, não `"Foo"`
Bug de produção aparece em input específico e surpreendente. Use Faker/Chance pra gerar dado que
se parece com produção; suba pra property-based testing quando quiser cobrir permutações.

```javascript
// ❌ "Foo" nunca dispara o regex (sem espaço) — falso verde
const r = addProduct("Foo", 5);
expect(r).toBe(true);

// ✅ input pseudo-real cobre paths que você não planejou
const r = addProduct(faker.commerce.productName(), faker.random.number());
expect(r).to.be.true;

// ✅✅ property-based (fast-check): roda 100 permutações automáticas
fc.assert(fc.property(fc.integer(), fc.string(), (id, name) => {
  expect(addNewProduct(id, name).status).toEqual("approved");
}));
```

### 7. Anti-flakiness: determinismo, sem sleep, dados por-teste
- **Não durma.** Em vez de `setTimeout`, use a espera determinística do framework (`cy.wait('@route')`,
  `waitFor`/`findBy` do testing-library, fake timers). Sleep é trade-off entre lento e flaky.

```javascript
// ❌ sleep/polling caseiro, sem timeout
const interval = setInterval(() => { if (getByText("the lion king")) clearInterval(interval); }, 100);

// ✅ espera determinística do framework
await waitFor(() => expect(getByText("the lion king")).toBeInTheDocument());
```

- **Dados isolados por teste.** Cada caso adiciona e age só nas próprias linhas de DB. Seed global é
  acoplamento: dois testes mutam o mesmo registro e o deploy aborta sem causa óbvia.

```javascript
// ❌ before(() => DB.AddSeedDataFromJson('seed.json'));  // o dado vive fora do teste
// ✅ cada teste cria o que precisa: const site = await SiteService.addSite({ name: faker.... });
```

- **Stub recurso flaky/lento** (API de backend) nos testes que não são E2E — staging cai sozinho e
  reprova seu componente que está correto; e chamada de rede deixa o teste ~20x mais lento.
- **Selector estável**: `data-test-id`, não classe CSS. Classe muda com o visual e derruba teste de lógica.

```javascript
// ❌ expect(wrapper.find("[className='d-flex-column']").text()).toBe("0");
// ✅ <span data-test-id="errorsLabel">{value}</span>  →  getByTestId("errorsLabel")
```

### 8. Pirâmide vs. troféu: enriqueça o portfólio, não empilhe só unit
A pirâmide de testes (muito unit, pouco E2E) ainda vale pra muita app — mas "todo modelo está
errado às vezes". App integração-cêntrica com pouca lógica não merece 50% do budget em unit. Combine
o **tipo de teste ao risco real** da feature, como um investidor diversifica por análise de risco.
O nível mais valioso e subestimado costuma ser o **component test** (o "troféu"): bate na API do
microsserviço, usa DB real (ou in-memory), e faz stub só do que é externo (outros serviços). Testa
o que você faz deploy, de fora pra dentro, com cobertura realista a custo razoável.

### 9. Meça a *qualidade* do teste, não só a cobertura — mutation testing
Cobertura tradicional mente: pode mostrar 100% e nenhuma função retornar o certo, porque ela mede
linha **visitada**, não **testada** (asserção certa). É como provar trabalho mostrando carimbos no
passaporte. Mutation testing (Stryker) **planta bugs** no código (`price === 0` vira `price != 0`)
e roda a suíte: se os testes passam mesmo assim, o mutante "sobreviveu" — seu teste não pega aquele
bug. Mutante morto = teste de verdade.

```javascript
// ❌ 100% de cobertura, 0% de teste — visita tudo, não verifica nada
function addNewOrder(o) { logger.log(o); DB.save(o); Mailer.sendMail(o.assignee, "..."); return { approved: true }; }
it("Test addNewOrder", () => { addNewOrder({ assignee: "j@x.com", price: 120 }); }); // sem expect
```

Complemente com **lint de teste** (`eslint-plugin-jest`/`-mocha`): pega teste sem asserção, teste
skipado, título duplicado — coisas que viram "90% verde" enganoso.

### 10. Garanta o contrato entre serviços — contract tests
Microsserviço com múltiplos clientes: você muda um campo e quebra um cliente importante. O provider
publica o typing da API (JSDoc/TypeScript) como pacote npm, e os consumidores ganham validação em
tempo de build. Abordagem mais rica: **PACT** — o cliente define as expectativas, grava num "broker",
e o servidor roda contra elas em todo build, pegando o mismatch cedo no CI.

### 11. Cubra os 5 resultados possíveis de um fluxo (foco no de fora, não no como)
Ao planejar o que verificar numa ação (ex.: chamada de API), cubra os cinco resultados observáveis
de fora: **(1) Response** (dados, schema, status HTTP) · **(2) Novo estado** (dado publicamente
acessível mudou) · **(3) Chamadas externas** (SMS, e-mail, cobrança) · **(4) Filas de mensagem**
(mensagem publicada) · **(5) Observabilidade** (erro tratado, log/métrica certos — o "usuário ops"
também é usuário).

## Checklist (qualquer "sim" é um tell a corrigir)

- [ ] Algum nome de teste não tem as 3 partes (o quê / cenário / esperado)?
- [ ] Tem teste sem separação AAA, ou com asserção imperativa (`if`/`forEach`/flag) em vez de `expect`?
- [ ] Algum teste verifica método privado / internal que não é requisito?
- [ ] Tem mock checando "foi chamado com esses args" em vez de stub/spy de comportamento?
- [ ] Input é `"Foo"`/`123` em vez de dado realista (Faker) ou property-based onde compensa?
- [ ] Tem `setTimeout`/sleep esperando async em vez de espera determinística do framework?
- [ ] Tem seed global de DB em vez de dado criado por-teste?
- [ ] Selector de teste de lógica depende de classe CSS em vez de `data-test-id`?
- [ ] Tem snapshot externo gigante em vez de inline curto (3–7 linhas) e focado?
- [ ] Existe teste sem nenhuma asserção (cobre linha, não testa nada)?
- [ ] A confiança vem só de "% de cobertura", sem mutation testing nos caminhos críticos?
- [ ] A estratégia é "só unit" mesmo numa feature integração-cêntrica?

## Tabela de decisão (use X quando Y)

| Quando Y | Use X | O que pega / o que custa |
|---|---|---|
| Lógica pura, sem IO (cálculo, classificação, validação) | **Unit test** (black-box, API pública) | Pega bug de lógica; barato e rápido, mas cobre pouca área por teste |
| Feature que cruza camadas de **um** serviço (rota → lógica → DB) | **Component test** (API real, DB real/in-memory, stub do externo) | Pega bug de integração interna com cobertura realista; melhor ROI no backend |
| Risco de mismatch de schema **entre** serviços/clientes | **Contract test** (typing publicado ou PACT) | Pega quebra de contrato no CI, antes do deploy; setup extra no provider+consumer |
| Validar fluxo ponta-a-ponta (front + backend real) | **E2E**, mas só **1–10** sobre staging production-like | Pega falha de deploy e de integração front↔back; caro, frágil, lento — use com parcimônia |
| Sanidade de produção / dev | **1 smoke E2E** que percorre o site map | ROI altíssimo, pega falha funcional/rede/empacotamento; não substitui teste funcional |
| Input com muitas permutações possíveis | **Property-based** (fast-check) sobre o unit | Acha o caso de borda que você não imaginou; roda N vezes, um pouco mais lento |
| Você quer saber se os testes **prestam** (não só cobrem) | **Mutation testing** (Stryker) nos caminhos críticos | Mede teste real (mutante morto); setup ~igual ao de cobertura, execução mais cara |
| Componente UI (lógica + alguns filhos, tamanho razoável) | **Render realista** (`mount`) + query por `data-test-id` | Pega bug que dado certo não chega na UI; evite shallow + chamar método interno |
| Recurso externo lento/instável num teste não-E2E (API backend) | **Stub** (nock/Sinon) | Mata flakiness e ~20x de lentidão; nunca stube o que você está justamente testando |
| Async com tempo desconhecido (animação, fetch) | Espera **determinística** do framework (`waitFor`, `cy.wait`, fake timers) | Determinismo sem sleep; sleep troca lentidão por flakiness |
| Asserção em markup que muda muito | **Inline snapshot** curto (3–7 linhas) sobre o trecho que importa | Self-explanatory e estável; snapshot externo gigante quebra por espaço/CSS |

## O que NÃO testar

- **Métodos privados / internals** — cobertos implicitamente pela API pública; testá-los só cria
  testes frágeis e custo de manutenção.
- **Como (interação interna)** quando o que importa é o **resultado observável** — não verifique
  "chamou o DAL com esses args"; verifique o efeito que é requisito.
- **100% de tudo** — 100% desloca o foco dos caminhos críticos pros cantos exóticos; ~80% é o número
  de bolso (Fowler: "nos 80s ou 90s altos"), e o limiar é contextual (A380 ≠ site de cartoon).
- **Detalhe gráfico (HTML/CSS) em teste de lógica** — separe UI de funcionalidade; asserte dado contra
  dado.
- **Documento gigante via snapshot externo** que ninguém leu — 1000 motivos pra falhar, zero sinal útil.
- Às vezes vale **dropar um teste** e trocar confiabilidade por agilidade — o Golden Rule manda manter
  a suíte enxuta e deliciosa de manter, não exaustiva por exaustão.

---
*Fonte: [goldbergyoni/javascript-testing-best-practices](https://github.com/goldbergyoni/javascript-testing-best-practices) — Yoni Goldberg. Exemplos de código extraídos do repositório real.*
