Un agregado que no podía volver a ser válido

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.

Leave a Comment