Test Conventions¶
Tronc commun unit + intégration — philosophie, gotchas Vitest Bricks, AI-slop checklist. Contrat intégration API : integration-test-conventions.mdc. Skills : upsert-unit-tests, upsert-integration-tests (création guidée), review-integration-tests (review du diff).
Stack¶
- API + common packages (
api,helpers,api-communication) : Vitest - Front apps (
front-app,front-mobile-app) : Jest (migration future) - DO NOT mix Jest and Vitest APIs in the same project
Scope : tester le custom, pas la lib¶
- DO tester la logique custom qu'on ajoute (refinements
.and(...), business functions, helpers maison, orchestration service) - DO NOT retester la librairie sous-jacente : pas de happy-path qui passerait avec ou sans la logique custom. Si un test passe que le refinement soit présent ou non, il teste la lib (ex.
idonttrustlikethat,zod,dayjs), pas ton code → le supprimer - Règle de pouce : chaque test doit tomber si on retire la logique custom qu'il est censé couvrir
- Unit test = logique pure uniquement. Mocker des modules internes (DB, services, providers) pour tester unitairement = mauvaise cible → test d'intégration (API) ou pas de test
- Frugalité : peu de tests mais représentatifs de la réalité sur laquelle on compte — pas de N variations du même chemin qui n'ajoutent aucune information ; N variations du même calcul → table-driven (
it.each)
Vitest — spécificités Bricks¶
- Globals (
describe,it,expect,vi, hooks) — pas d'import - Fake timers activés globalement via
src/test-setup.ts; instant précis →vi.setSystemTime(new Date(2024, 0, 15)), reset enafterEachsi le test change l'heure - DO NOT
jest.fn()/jest.mock()/jest.spyOn()— n'existent pas en Vitest →vi.*; DO NOT assertions chai (expect(...).to.equal()) - File naming : API
*.unit-test.ts, common packages*.test.ts/*.spec.ts— dans des dossiers__test__/ou__tests__/ - Config : root
vitest.config.base.tspartagé, chaque projetmergeConfig(base, defineProject({ ... })); types vitest danstsconfig.spec.json(jamais danstsconfig.build.json)
Littéraux de date¶
new Date('2024-01-01') — l'ISO date-only vaut minuit UTC (spec ECMAScript), déterministe sur toute machine/CI. Pas de suffixe T00:00:00.000Z (bruit), pas de dayjs('2024-01-01') (parse en heure locale → flake CI vs local ; dayjs.utc(...).toDate() est plus long pour zéro gain). Besoin d'un instant intra-journée → forme complète avec zone ('2024-01-01T09:30:00Z').
Integration tests (API)¶
API integration tests live next to the code they test (src/<module>/__tests__/<feature>.integration-test.ts) and run against a local Postgres + Redis stack (docker compose).
Contract: API integration test conventions. Guided creation + full how-to (fixtures, auth, MSW, cron/worker, dev loop): upsert-integration-tests skill. Diff review: review-integration-tests skill.
AI-slop checklist¶
Patterns que l'IA produit par défaut sur les tests — rejeter en review :
- ❌
expect(res.body.x).toBe(...)× N → un seultoMatchObject({ ... }) - ❌ Snapshot d'une réponse HTTP → inline le JSON attendu (sinon
--update-snapshotscimente le bug) - ❌ Asserter la copie FR (
'Compte introuvable.') → assert surstatusCode+codebusiness, pas la trad - ❌
try { ... } catch (e) { expect(e).toBeDefined() }→await expect(...).rejects.toThrow(SomeError) - ❌ Mock du service que le controller appelle dans un test d'intégration → c'est un test unitaire déguisé
- ❌
it('works'),it('returns 200'),it('handles errors')→ nommer l'observable précis - ❌
db.{selectFrom,insertInto,updateTable,deleteFrom}(...)dans*.integration-test.ts→ reads via<Module>Repository, writes viaseed<Entity>fixture - ❌ URL string hardcodée pour une route Bricks (
'/investor/portfolio/...') → import du descriptorapi-communication[-bricksoffice](exception : better-auth) - ❌ Happy-path only → chaque route a ≥ 1
Quand <erreur> - ❌ Asserter via le même GET que le SUT exerce → utiliser TestApi pour les side-effects (sinon test tautologique)
- ❌ Générer un test à partir du code sans connaître l'attendu → cimente le bug comme spec (cf. KeelCode 2025)