SaaS Starter

Seed & test

Semeando o database de dev e os padrões de testes que realmente pegam regressões.

Seed

cd apps/server && bun run seed

O seed vive em apps/server/scripts/seed.ts. Cria:

  • Um usuário super-admin ([email protected] / admin1234).
  • Uma organização com o admin como owner.
  • As linhas default de roles (Owner / Admin / Member) com um permission grant por role retirado do catálogo @app/shared.

Rodar o seed novamente é idempotente: linhas existentes são upserted, não duplicadas.

Para desenvolvimento local de features, um seed te dá um admin logado em segundos — sem precisar percorrer o flow de register a cada vez.

Camadas de teste

O boilerplate roda três camadas de testes, cada uma exercitando uma surface diferente:

Domain & application — Vitest, em memória

Testes unitários puros para aggregates e use cases. Sem Prisma, sem Express. Injete fakes em memória para repositories e o event bus.

test('User.create rejects empty email', () => {
  const result = User.create({ email: '', /* ... */ });
  expect(result.isErr()).toBe(true);
  expect(result.error.code).toBe('USER_EMAIL_INVALID');
});

Run: bun run test --filter=@app/server (Vitest). Esta deve ser a camada maior.

Infrastructure — Vitest com DB real

Testes de repository rodam contra um Postgres real (ou sqlite por velocidade se suas queries são portáveis). Valem a fricção extra pela camada de mapping — a tradução row-to-aggregate é onde regressões de forma de linha se escondem.

O módulo de eventos também roda com o InMemoryEventBus real em vez de um stub para que a ordem subscribe/publish seja exercitada.

HTTP — supertest

Monte o router real com um test container. Faça asserts em status codes, formas de response, e pares comportamentais:

  • "usuário sem grant pega 403, usuário com grant pega 200"
  • "campo faltando retorna 422 com issues[].path apontando para a key faltante"
  • "segundo POST com a mesma idempotency key retorna a response original, não uma duplicada"

Esses testes valem quando cada teste não poderia passar se o código de produção parasse de fazer a coisa certa.

End-to-end — Playwright

tests/ na raiz do repo. Sobe a app inteira e clica por flows reais: register, log in, criar uma organização, convidar um membro. Lentos; reserve-os para jornadas que você não pode quebrar.

O que "challenging" significa

A regra do CLAUDE.md: um teste que não falharia quando a lógica regride não vale a pena escrever.

Coverage theater se parece com:

test('createUser returns user', async () => {
  const result = await createUser({ email: '[email protected]', name: 'A', password: 'p' });
  expect(result.isOk()).toBe(true);
});

Se o use case começa a retornar o usuário errado, esse teste ainda passa. Substitua por um que afirme que o usuário foi realmente persistido (via o fake repo), ou um que afirme que o evento user.created dispara com o payload certo.

BullMQ end-to-end

Pule ioredis-mock para BullMQ — ele não roda o Lua do BullMQ. Ou:

  1. Suba um Redis real (Docker ou Railway-staging) e rode a suite ali, ou
  2. Faça unit-test do produtor (afirma que o wrapper chamou queue.add com o payload certo) e do processor (afirma que faz a coisa certa para um job dado) separadamente.

Na maioria das vezes a opção 2 basta.

Locale e i18n

Ao testar um controller que retorna copy traduzida, defina Accept-Language no test request. O runtime de i18n server-side em apps/server/src/i18n/ resolve o dicionário a partir dos headers; testes não devem bater no locale default por acidente.

Nesta página