Testing
Escopo: TypeScript. Visão transversal: shared/standards/testing.md.
Os padrões de teste do JavaScript continuam valendo. O TypeScript acrescenta uma checagem que roda
antes do teste: a fixture (massa de teste) declarada com satisfies (operador de
conformidade) é conferida contra o tipo real na compilação, e o mock (dados fictícios) que não
cumpre a interface é acusado ali mesmo. Os testes deixam de quebrar por causa de uma massa de dados
desatualizada, porque a massa desatualizada nem chega a rodar.
Base JavaScript: javascript/conventions/advanced/testing.md
Usa Vitest nos exemplos. Mesma API (Application Programming Interface · Interface de Programação de Aplicações) do Jest, integração nativa com TypeScript.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| AAA (Arrange, Act, Assert · Arranjar, Agir, Atestar) | Estrutura em três fases: preparar contexto, executar comportamento, verificar resultado |
| fixture (massa de teste) | Dado de entrada conhecido reutilizado entre testes; tipado com satisfies |
satisfies (operador de conformidade) | Valida que a fixture cumpre o tipo sem alargar a tipagem inferida |
| mock (dados fictícios) | Objeto falso que substitui dependência real e devolve respostas pré-definidas |
| stub (resposta fixa) | Substituto simples que retorna valor fixo, sem registrar chamadas |
| type-only test (teste só de tipo) | Verificação de comportamento do compilador (expectTypeOf); não roda em runtime |
Vitest (test runner com TS nativo) | Runner moderno com integração TypeScript, compatível com a API do Jest |
| expressive naming (nomeação expressiva) | Variáveis de assert com nome do conceito (actualPrice, expectedName) |
A massa de teste é conferida com satisfies
Uma massa de teste escrita como objeto solto envelhece em silêncio. O campo total vira
obrigatório na entidade, e o objeto do teste continua sem ele, porque nada liga os dois. O teste
passa, e o que ele está testando deixou de existir.
satisfies User liga a massa ao tipo real. Campo que falta e campo com tipo errado viram erro de
compilação, e a massa continua sendo o objeto literal que era, com cada valor no seu tipo exato.
❌ Ruim: a massa não tem tipo, e o campo errado passa despercebido
test("applies discount to order", () => {
const order = {
id: "ord-1",
customrId: "cust-99", // typo: sem erro de compilação
total: 100,
};
const actualOrder = applyDiscount(order, 10);
const expectedTotal = 90;
expect(actualOrder.total).toBe(expectedTotal);
});
✅ Bom: satisfies confere a massa contra o tipo real na compilação
test("applies 10% discount to order total", () => {
const order = {
id: "ord-1",
customerId: "cust-99",
total: 100,
} satisfies Order;
const actualOrder = applyDiscount(order, 10);
const expectedTotal = 90;
expect(actualOrder.total).toBe(expectedTotal);
});
O mock implementa a interface de verdade
Um mock escrito como objeto qualquer só tem os métodos que o teste de hoje usa. Quando alguém acrescenta um método na interface, o mock fica incompleto, e ninguém fica sabendo: o teste continua passando contra um dublê que já não representa a dependência real.
Declarar o mock como UserRepository faz o compilador conferir a lista inteira de métodos e as
assinaturas de cada um. O método novo aparece como erro no teste, no mesmo commit em que foi criado.
❌ Ruim: o mock é um objeto solto, e não acompanha a interface
test("saves order and sends notification", async () => {
const mockRepo = {
save: vi.fn().mockResolvedValue(undefined),
findById: vi.fn(), // não usado: mas sem contrato, ninguém sabe se está faltando algo
};
const service = new OrderService(mockRepo as any);
await service.createOrder({ customerId: "cust-1", total: 200 });
expect(mockRepo.save).toHaveBeenCalledOnce();
});
✅ Bom: o mock declara a interface, e o compilador confere método por método
test("saves order on creation", async () => {
const mockRepo: IOrderRepository = {
save: vi.fn().mockResolvedValue(undefined),
findById: vi.fn().mockResolvedValue(null),
findByCustomer: vi.fn().mockResolvedValue([]),
};
const service = new OrderService(mockRepo);
await service.createOrder({ customerId: "cust-1", total: 200 });
expect(mockRepo.save).toHaveBeenCalledOnce();
});
expectTypeOf testa o tipo, e não o valor
Algumas funções existem para preservar tipos, e o teste comum não alcança isso. Um genérico que
devolve TItem a partir de TItem[] pode estar devolvendo any sem que nenhuma asserção de
valor perceba, porque any passa em qualquer comparação.
expectTypeOf (do Vitest) verifica o tipo que o compilador inferiu. Ele é a forma de testar as
funções utilitárias de tipo e de garantir que o genérico chega ao retorno como deveria.
✅ Bom: expectTypeOf confere o tipo que o compilador inferiu
import { expectTypeOf } from "vitest";
test("findById returns Order or null", () => {
const result = findById("ord-1");
expectTypeOf(result).toEqualTypeOf<Promise<Order | null>>();
});
test("applyDiscount preserves Order shape", () => {
const order = { id: "ord-1", customerId: "cust-1", total: 100 } satisfies Order;
const result = applyDiscount(order, 10);
expectTypeOf(result).toEqualTypeOf<Order>();
});
O teste verifica qual erro foi lançado
expect(fn).rejects.toThrow() passa quando qualquer erro sobe, inclusive um TypeError causado
por um bug no próprio teste. O teste fica verde por um motivo errado, e continua verde quando a
regra de negócio que ele deveria proteger some.
Verificar a classe do erro e a mensagem amarra o teste ao comportamento que interessa: o pedido
inexistente lança NotFoundError, e nada além disso conta como sucesso.
❌ Ruim: o teste aceita qualquer erro, inclusive um bug do próprio teste
test("throws on invalid order", async () => {
await expect(createOrder({ customerId: "", total: -1 })).rejects.toThrow();
});
✅ Bom: a classe do erro e a mensagem são verificadas
test("throws ValidationError when total is negative", async () => {
const invalidInput = { customerId: "cust-1", total: -1 };
const actual = createOrder(invalidInput);
await expect(actual).rejects.toThrow(ValidationError);
});
DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.