testing : formaliser les factories de state d'agrégat dans les tests #18

Closed
opened 2026-07-04 12:10:58 +00:00 by momsse · 0 comments
Owner

Migré depuis viziertronic/octant#38 — ouvert le 2026-06-11 par @momsse.

Issue de la review de la PR 04 (#34) — le pattern des factories de state commence à proliférer.

Constat

Chaque fichier de test d'agrégat redéfinit à la main ses factories de state en chaînant aggregate.evolve :

  • user.aggregate.test.ts (PR 04) : 7 factories (activeAliceState, aliceWithSessionState, aliceWithDiscordLinkedState, aliceWithDiscordTokensState, suspendedAliceState, anonymizedAliceState, deletedAliceState)
  • Sur feat/authentication, le même pattern se répète dans les 5 tests d'agrégats authorization (PRs 7-9) : activeAdminRoleState, defaultAdminRoleState, deletedAdminRoleState, etc.
  • Les tests de use-cases (PR 05) re-sèment les mêmes états de départ via les repositories in-memory

Coûts : duplication des chaînes evolve(evolve(...)) illisibles au-delà de 2 events, fixtures divergentes entre le test d'agrégat et les tests de use-cases pour « le même » état métier, et aucun endroit canonique où trouver « un user actif avec un compte Discord lié ».

Proposition

Deux étages :

  1. Helper générique dans @octant/event-sourcing (ou @octant/testing) — les agrégats exposent déjà reconstitute(events, emptyState), il manque juste l'idiome :
export function stateFromEvents<Id, State, Event>(
  aggregate: AggregateShape<Id, State, Event>,
  id: Id,
  events: ReadonlyArray<Event>,
): State

Les chaînes evolve(evolve(...)) deviennent une liste plate d'events — déjà plus lisible sans rien inventer.

  1. Fixtures nommées par agrégat, colocalisées avec les tests (user.fixtures.ts à côté de user.aggregate.test.ts, suffixe .fixtures.ts à ajouter aux conventions du CLAUDE.md « Domain package layout ») : le fichier exporte les factories nommées (activeAliceState(), …) construites sur le helper, et les tests de use-cases importent les mêmes fixtures que le test d'agrégat — un seul endroit définit ce qu'est « un user actif avec un compte Discord ».

À arbitrer : fonction libre vs builder fluent (givenUser(aliceId).created().linkedTo('discord').suspended()) — le builder est plus lisible pour les longs scénarios mais c'est une DSL à maintenir ; commencer par la liste plate d'events suffit probablement.

Timing

Applicable dès la PR 05 (les tests de use-cases en profitent immédiatement) et rentabilisé sur les PRs 7-9 (5 agrégats authorization). Peut se faire en PR dédiée courte après le merge de #34, ou se glisser dans #16 (déjà dans @octant/event-sourcing).

> _Migré depuis [viziertronic/octant#38](https://github.com/viziertronic/octant/issues/38) — ouvert le 2026-06-11 par @momsse._ Issue de la review de la PR 04 (#34) — le pattern des factories de state commence à proliférer. ## Constat Chaque fichier de test d'agrégat redéfinit à la main ses factories de state en chaînant `aggregate.evolve` : - `user.aggregate.test.ts` (PR 04) : **7 factories** (`activeAliceState`, `aliceWithSessionState`, `aliceWithDiscordLinkedState`, `aliceWithDiscordTokensState`, `suspendedAliceState`, `anonymizedAliceState`, `deletedAliceState`) - Sur `feat/authentication`, le même pattern se répète dans les **5 tests d'agrégats authorization** (PRs 7-9) : `activeAdminRoleState`, `defaultAdminRoleState`, `deletedAdminRoleState`, etc. - Les tests de use-cases (PR 05) re-sèment les mêmes états de départ via les repositories in-memory Coûts : duplication des chaînes `evolve(evolve(...))` illisibles au-delà de 2 events, fixtures divergentes entre le test d'agrégat et les tests de use-cases pour « le même » état métier, et aucun endroit canonique où trouver « un user actif avec un compte Discord lié ». ## Proposition Deux étages : 1. **Helper générique** dans `@octant/event-sourcing` (ou `@octant/testing`) — les agrégats exposent déjà `reconstitute(events, emptyState)`, il manque juste l'idiome : ```ts export function stateFromEvents<Id, State, Event>( aggregate: AggregateShape<Id, State, Event>, id: Id, events: ReadonlyArray<Event>, ): State ``` Les chaînes `evolve(evolve(...))` deviennent une liste plate d'events — déjà plus lisible sans rien inventer. 2. **Fixtures nommées par agrégat**, colocalisées avec les tests (`user.fixtures.ts` à côté de `user.aggregate.test.ts`, suffixe `.fixtures.ts` à ajouter aux conventions du CLAUDE.md « Domain package layout ») : le fichier exporte les factories nommées (`activeAliceState()`, …) construites sur le helper, et **les tests de use-cases importent les mêmes fixtures** que le test d'agrégat — un seul endroit définit ce qu'est « un user actif avec un compte Discord ». À arbitrer : fonction libre vs builder fluent (`givenUser(aliceId).created().linkedTo('discord').suspended()`) — le builder est plus lisible pour les longs scénarios mais c'est une DSL à maintenir ; commencer par la liste plate d'events suffit probablement. ## Timing Applicable dès la PR 05 (les tests de use-cases en profitent immédiatement) et rentabilisé sur les PRs 7-9 (5 agrégats authorization). Peut se faire en PR dédiée courte après le merge de #34, ou se glisser dans #16 (déjà dans `@octant/event-sourcing`).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
momsse/octant#18
No description provided.