Testing

Escopo: JavaScript. Visão transversal: shared/standards/testing.md.

Um teste é a documentação viva do comportamento esperado, e a única que não mente porque roda. Quando falha, conta a história inteira: quem foi chamado, com o quê, e o que deveria ter voltado. Em JavaScript a base é node:test mais node:assert/strict (ambos nativos desde o Node 18), e a estrutura é o AAA (Arrange, Act, Assert · Arranjar, Agir, Atestar): as declarações ficam agrupadas e uma linha em branco isola a asserção do resto.

Conceitos fundamentais

ConceitoO que é
AAA (Arrange, Act, Assert · Arranjar, Agir, Atestar)Estrutura em três fases: preparar contexto, executar comportamento, verificar resultado
unit test (teste unitário)Testa uma função ou classe isolada; rápido; dependências externas são substituídas
integration test (teste de integração)Testa múltiplas peças juntas, incluindo banco real ou HTTP de teste
fixture (massa de teste)Dado de entrada conhecido reutilizado entre testes
mock (dados fictícios)Objeto falso que substitui dependência real (banco, API, relógio) e devolve respostas pré-definidas; isola o teste do mundo externo
stub (resposta fixa)Substituto simples que retorna valor fixo, sem registrar chamadas
spy (espião)Invólucro que registra chamadas mas mantém o comportamento original
assertion (asserção)Verificação explícita do resultado esperado (assert.strictEqual, assert.deepStrictEqual)
actual (valor atual)O que o código devolveu de fato na execução do teste; é o valor que está sob verificação
expected (valor esperado)O que o código deveria ter devolvido; é o valor que você escreve à mão no teste, como referência
expressive naming (nomeação expressiva)Variáveis de assert com nome do conceito (actualPrice, expectedName), nunca genéricos

O code style se aplica dentro dos testes. O assert recebe variáveis nomeadas de forma expressiva (actualPrice, expectedName), sem expressões, acessos de propriedade ou literais inline.

Os exemplos usam node:test e node:assert/strict: nativos desde o Node 18, sem dependências externas.

import { test, describe } from "node:test";
import assert from "node:assert/strict";
Note

Em node:assert, a convenção é assert.strictEqual(actual, expected): actual primeiro. Em Jest e Vitest, a API fluente deixa a ordem explícita: expect(actual).toBe(expected).

Fases misturadas: AAA

Cada teste agrupa as três fases (contexto, execução e valor esperado) em um bloco único, e uma linha em branco isola a asserção do resultado.

❌ Ruim: tudo inline, fases invisíveis
test("applies discount", () => {
  assert.strictEqual(applyDiscount({ price: 100, discountPercentage: 10 }), 90);
});
✅ Bom: setup agrupado, asserção isolada por linha em branco
test("applies 10% discount to order price", () => {
  const order = { price: 100, discountPercentage: 10 }; // arrange
  const actualPrice = applyDiscount(order); // act
  const expectedPrice = 90; // assert

  assert.strictEqual(actualPrice, expectedPrice);
});

Assert com valores soltos

Nomeie actual e expected antes de comparar. O assert passa a ler como uma frase em vez de um cálculo, e a mensagem de falha diz o que era esperado sem você abrir o arquivo. A regra vale sempre: mesmo quando o valor já tem nome, declare expected de forma explícita para o assert não deixar dúvida.

❌ Ruim: literais inline, falha não diz o que era esperado
test("formats full name", () => {
  assert.strictEqual(formatName({ first: "John", last: "Doe" }), "John Doe");
});

test("returns active users only", () => {
  const users = [
    { name: "Alice", active: true },
    { name: "Bob", active: false },
  ];
  assert.deepStrictEqual(filterActive(users), [
    { name: "Alice", active: true },
  ]);
});
✅ Bom: expected e actual declarados, assert semântico
test("formats full name", () => {
  const user = { first: "John", last: "Doe" };
  const actualName = formatName(user);
  const expectedName = "John Doe";

  assert.strictEqual(actualName, expectedName);
});

test("returns active users only", () => {
  const users = [
    { name: "Alice", active: true },
    { name: "Bob", active: false },
  ];

  const actualUsers = filterActive(users);
  const expectedUsers = [{ name: "Alice", active: true }];

  assert.deepStrictEqual(actualUsers, expectedUsers);
});

Nome genérico

O nome do teste descreve o cenário e o resultado esperado, não o nome da função nem uma afirmação vaga. Sem prefixos: should não agrega informação e given/when/then é mecânico e verboso.

❌ Ruim: prefixo vazio, nome que repete a implementação
test("test 1", () => {
  /* ... */
});
test("should apply discount", () => {
  /* ... */
});

test("applyDiscount function", () => {
  /* ... */
});
✅ Bom: cenário + resultado esperado, sem prefixo
test("applies discount when order total exceeds minimum", () => {
  /* ... */
});
test("returns original price when no discount applies", () => {
  /* ... */
});

test("throws ValidationError when discount percentage is negative", () => {
  /* ... */
});

Estado compartilhado

Cada teste monta o próprio contexto e não depende de nenhum outro para funcionar. Estado que sobrevive de um teste para o seguinte produz a falha mais cara de depurar: o teste passa sozinho, quebra dentro da suíte, e o culpado é a ordem de execução.

❌ Ruim: estado compartilhado que muda entre testes
let order;

test("creates order", () => {
  order = createOrder({ items: [{ id: 1, price: 50, quantity: 2 }] });

  assert.ok(order.id);
});

test("applies discount to order", () => {
  const actual = applyDiscount(order, 10); // depende do teste anterior
  const actualPrice = actual.price;

  const expected = 45;
  assert.strictEqual(actualPrice, expected);
});
✅ Bom: cada teste isolado, sem dependência de execução
test("creates order with generated id", () => {
  const order = createOrder({ items: [{ id: 1, price: 50, quantity: 2 }] });
  const actualId = order.id;

  assert.ok(actualId);
});

test("applies 10% discount to order price", () => {
  const order = { items: [{ id: 1, price: 50, quantity: 2 }], total: 100 };
  const actualOrder = applyDiscount(order, 10);
  const actualPrice = actualOrder.price;
  const expectedPrice = 90;

  assert.strictEqual(actualPrice, expectedPrice);
});

Exceção sem tipo

Testar que um erro foi lançado é diferente de testar qual erro foi lançado. assert.rejects confere o tipo e a mensagem do erro, além da presença dele.

❌ Ruim: try/catch manual, tipo não verificado
test("throws on missing order", async () => {
  try {
    await findOrder(null);
  } catch (error) {
    assert.ok(error); // qualquer erro passa
  }
});
✅ Bom: assert.rejects com matcher de tipo
test("throws NotFoundError when order does not exist", async () => {
  const invalidId = "nonexistent-id";
  const actual = findOrder(invalidId);
  const expected = { name: "NotFoundError" };

  await assert.rejects(actual, expected);
});

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.