Seed & test
Sembrar la base de datos de dev y los patrones de tests que realmente atrapan regresiones.
Seed
cd apps/server && bun run seedEl seed vive en apps/server/scripts/seed.ts. Crea:
- Un user super-admin (
[email protected]/admin1234). - Una organización con el admin como owner.
- Las filas default de roles (Owner / Admin / Member) con un permission grant por rol tomado del catálogo
@app/shared.
Re-correr el seed es idempotente: las filas existentes se upsertean, no se duplican.
Para desarrollo local de features, un seed te da un admin logueado en segundos — no hace falta caminar el flow de register cada vez.
Capas de tests
El boilerplate corre tres capas de tests, cada una ejercita una surface distinta:
Domain & application — Vitest, en memoria
Tests unitarios puros para aggregates y use cases. Sin Prisma, sin Express. Inyectá fakes en memoria para repositories y el 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 debería ser la capa más grande.
Infrastructure — Vitest con DB real
Los tests de repository corren contra un Postgres real (o sqlite por velocidad si tus queries son portables). Valen la fricción extra para la capa de mapping — la traducción row-to-aggregate es donde se esconden las regresiones de forma de fila.
El módulo de eventos también corre con el InMemoryEventBus real en lugar de un stub para que el orden subscribe/publish se ejercite.
HTTP — supertest
Montá el router real con un test container. Hacé asserts en status codes, formas de response, y pares behaviorales:
- "user sin grant obtiene 403, user con grant obtiene 200"
- "campo faltante devuelve 422 con
issues[].pathapuntando a la key faltante" - "segundo POST con la misma idempotency key devuelve la response original, no un duplicado"
Estos tests valen cuando cada test no podría pasar si el código de producción dejó de hacer lo correcto.
End-to-end — Playwright
tests/ en la raíz del repo. Levanta la app completa y clickea por flows reales: register, log in, crear una organización, invitar un miembro. Lentos; reservalos para journeys que no podés romper.
Qué significa "challenging"
La regla del CLAUDE.md: un test que no fallaría cuando la lógica regresa no vale la pena escribirlo.
El coverage theater se ve así:
test('createUser returns user', async () => {
const result = await createUser({ email: '[email protected]', name: 'A', password: 'p' });
expect(result.isOk()).toBe(true);
});Si el use case empieza a devolver el user incorrecto, este test sigue pasando. Reemplazalo con uno que asegure que el user fue realmente persistido (vía el fake repo), o uno que asegure que el evento user.created dispara con el payload correcto.
BullMQ end-to-end
Salteá ioredis-mock para BullMQ — no corre el Lua de BullMQ. O bien:
- Levantá un Redis real (Docker o Railway-staging) y corré la suite ahí, o
- Hacé unit-test del productor (chequea que el wrapper llamó a
queue.addcon el payload correcto) y del processor (chequea que hace lo correcto para un job dado) por separado.
La mayoría de las veces la opción 2 alcanza.
Locale e i18n
Cuando testees un controller que devuelve copy traducida, seteá Accept-Language en el test request. El runtime de i18n server-side en apps/server/src/i18n/ resuelve el diccionario desde headers; los tests no deberían pegar al locale default por accidente.