Cómo probar un agregado

Escribir pruebas para un agregado es fácil de hacer mal, porque el camino obvio —construir, comprobar que falla— cubre poco y tranquiliza mucho. Estas son las que de verdad encuentran cosas.

1. Que recolecta, no que lanza

it(\'recolecta todas las reglas rotas, no sólo la primera\', () => {
  const product = new Product({
    name: Name.create(\'Teclado Inalámbrico\'),
    description: Description.create(\'Corta\'),   // más corta que el nombre
    price: Price.create(49.99),
    status: ProductStatus.ACTIVE,
  });

  expect(product.isValid).toBe(false);
  expect(product.brokenRules.getBrokenRules()).toHaveLength(1);
});

Fíjate en que se usa new y no create(). La fábrica lanza; para observar la recolección hay que construir directamente. Y los value objects que se le pasan tienen que ser válidos: si le das un Name.create(\'Ab\'), la excepción salta ahí y nunca llegas al agregado.

2. Que vuelve a ser válido — la que nadie escribe

it(\'vuelve a ser válido cuando se corrige el problema\', () => {
  const product = new Product({ ...props, price: Price.create(0) });
  expect(product.isValid).toBe(false);

  product.changePrice(Price.create(49.99));

  expect(product.isValid).toBe(true);                        // ← ésta
  expect(product.brokenRules.getBrokenRules()).toHaveLength(0);
});

La primera aserción pasa siempre. La tercera es la que tiene valor, y es la que encontró el peor bug de la librería: validate() sólo añadía reglas y nunca las limpiaba, así que un agregado que fallaba una vez no volvía a ser válido jamás.

Un test que sólo verifica el camino de fallo no distingue entre «detecta el error» y «se queda atascado en el error».

3. Las transiciones que NO se permiten

it.each([
  [\'DRAFT\',     \'ship\'],
  [\'DRAFT\',     \'deliver\'],
  [\'DELIVERED\', \'cancel\'],
])(\'no permite %s → %s\', (estado, accion) => {
  const order = ordenEn(estado);
  expect(() => order[accion]()).toThrow(InvalidStateTransitionException);
});

Las transiciones permitidas se prueban solas al escribir el caso feliz. Las prohibidas no las prueba nadie, y son exactamente donde vive el bug de negocio caro: el pedido que se puede cancelar después de entregado.

4. Que los eventos se acumulan

it(\'registra el evento al confirmar\', () => {
  const order = ordenConArticulos();

  order.confirm();

  const eventos = order.getUncommittedEvents();
  expect(eventos).toHaveLength(1);
  expect(eventos[0]).toBeInstanceOf(OrderConfirmedEvent);
});

El agregado recolecta eventos; despacharlos es cosa del handler. Probar aquí que el evento existe, y en el handler que se llama a commit(), cubre las dos mitades sin que ninguna prueba tenga que montar el bus entero.

5. Que load() NO valida

it(\'rehidrata sin validar, para poder leer filas antiguas\', () => {
  // Una fila guardada bajo reglas anteriores, que hoy sería inválida.
  const order = Order.load({ ...props, items: [] }, id);

  expect(order).toBeDefined();     // se puede leer
  expect(order.isValid).toBe(false); // y saber que no cumple hoy
});

Es contraintuitivo y es correcto. Si load() validara, endurecer una regla convertiría tus filas históricas en datos que no se pueden ni abrir para migrarlos.

Y una advertencia sobre la cobertura

Cuando estas pruebas estén escritas, mira el informe sin filtros:

npx jest --coverage --collectCoverageFrom=\'**/*.ts\' --coverageReporters=text
node -p \"Object.keys(require(\'./coverage/coverage-final.json\')).length\"

Si esperabas 40 ficheros y salen 26, el problema no es el porcentaje: es que catorce ni siquiera se están midiendo. Es una historia larga, y le pasó a esta librería con un 98,6 % declarado sobre un 58,4 % real.

Leave a Comment