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.
