De los 34 bugs que encontramos al escribir pruebas para ddd-lib, éste era el más grave y también el más corto de explicar.
El síntoma
Un usuario envía un formulario con el precio a 0. Recibe, correctamente, la regla rota. Lo corrige, pone 49.99, reenvía.
Y recibe exactamente la misma respuesta.
La causa
public validate(): void {
this._guardStrategy();
// ...ejecuta los validadores, que van llamando a addBrokenRule()
}
Falta una línea. validate() sólo añadía. Nunca limpiaba. Cada pasada acumulaba sobre la anterior, así que un agregado que falló una vez arrastraba esa regla para siempre, aunque la condición que la produjo ya no se cumpliera.
public validate(): void {
// Descarta los hallazgos de la pasada anterior antes de re-derivarlos.
this.brokenRules.clear();
this._guardStrategy();
// ...
}
Por qué nadie lo vio
Porque el patrón de uso dominante lo esconde:
const product = Product.create(name, description, price); // valida UNA vez
Se construye el objeto, se valida, y si falla se lanza y se descarta. Nunca se valida dos veces el mismo objeto. En ese camino el bug no existe.
Sólo aparece cuando algo revalida: un agregado de vida larga que se modifica y se vuelve a comprobar, un formulario que reintenta contra la misma instancia, un test que llama a validate() dos veces. Y los tests no lo hacían, porque se escribieron imitando el flujo normal.
El patrón de test que lo caza
it(\'vuelve a ser válido cuando se corrige el problema\', () => {
const product = new Product({ name, description, price: Price.create(0), status });
expect(product.isValid).toBe(false);
product.changePrice(Price.create(49.99));
expect(product.isValid).toBe(true); // ← el que fallaba
expect(product.brokenRules.getBrokenRules()).toHaveLength(0);
});
La primera aserción pasa siempre. Es la segunda la que tiene valor, y es la que casi nunca se escribe, porque comprobar que algo deja de estar mal se siente redundante cuando ya comprobaste que estaba mal.
No lo es. Un test que sólo verifica el camino de fallo no distingue entre «detecta el error» y «se queda atascado en el error».
Una regla general
Si tienes un método que deriva un resultado del estado actual —validación, cálculo de totales, resolución de permisos— hazte una pregunta: ¿qué pasa si lo llamo dos veces?
Debería dar lo mismo si el estado no cambió, y algo distinto si cambió. Si el segundo resultado depende de que hubo un primero, tienes un acumulador donde querías una función. Busca .clear(), = [] o .reset() al principio de esos métodos: cuando falta uno, casi nunca es a propósito.
