Guía práctica
El test que evita que un Refresh fallido borre la caché
Un test de contrato para una garantía fácil de romper: si la fuente falla durante Refresh, el último valor válido debe seguir disponible.
Una caché ya contiene la versión v1 de un perfil. Llega el momento de actualizarla, pero la fuente no responde. ¿Qué debe devolver el siguiente Get?
La firma de los métodos no decide la respuesta:
Refresh(ctx context.Context, key string) (Value, error)
Get(key string) (Value, bool)Hay al menos dos comportamientos posibles:
Refreshfalla y eliminav1;Refreshfalla y conservav1como último valor válido.
Si los consumidores necesitan seguir operando con datos anteriores durante un fallo transitorio, la segunda opción es una garantía de la caché. Debe estar escrita y debe tener un test.
El test incompleto que parece suficiente#
Este caso verifica que el error cruza la API:
func TestRefreshReturnsSourceError(t *testing.T) {
source := &stubSource{err: ErrUnavailable}
cache := NewCache(source)
_, err := cache.Refresh(context.Background(), "profile:42")
if !errors.Is(err, ErrUnavailable) {
t.Fatalf("expected source error, got %v", err)
}
}Es correcto, pero no prueba la garantía importante. Una implementación puede devolver exactamente ese error después de borrar un valor válido. El test pasa mientras el consumidor pierde su respaldo.
Construye el estado anterior y observa el posterior#
El caso completo establece primero un valor exitoso, provoca el fallo y vuelve a consultar mediante la API pública:
func TestFailedRefreshPreservesPreviousValue(t *testing.T) {
ctx := context.Background()
source := &stubSource{value: Value("v1")}
cache := NewCache(source)
if _, err := cache.Refresh(ctx, "profile:42"); err != nil {
t.Fatalf("seed cache: %v", err)
}
source.err = ErrUnavailable
if _, err := cache.Refresh(ctx, "profile:42");
!errors.Is(err, ErrUnavailable) {
t.Fatalf("expected source error, got %v", err)
}
got, ok := cache.Get("profile:42")
if !ok {
t.Fatal("expected previous value to remain cached")
}
if got != Value("v1") {
t.Fatalf("expected v1, got %q", got)
}
}El stub controla una condición necesaria: la respuesta de la fuente. Las afirmaciones, en cambio, se hacen sobre lo que un consumidor puede observar en Cache: el error de Refresh y el valor posterior de Get.
La implementación incorrecta que detecta#
Este orden parece razonable si alguien piensa que actualizar significa vaciar y volver a cargar:
func (c *Cache) Refresh(ctx context.Context, key string) (Value, error) {
delete(c.items, key)
value, err := c.source.Load(ctx, key)
if err != nil {
return "", err
}
c.items[key] = value
return value, nil
}El test que solo comprueba el error lo acepta. TestFailedRefreshPreservesPreviousValue falla porque el segundo Get ya no encuentra v1.
Una implementación que cumple la garantía obtiene primero el nuevo valor y reemplaza el anterior solo después del éxito:
func (c *Cache) Refresh(ctx context.Context, key string) (Value, error) {
value, err := c.source.Load(ctx, key)
if err != nil {
return "", err
}
c.items[key] = value
return value, nil
}El test no exige este algoritmo. Otra implementación podría usar versiones, una transacción o un intercambio atómico. Todas pasan si conservan la misma observación pública.
La plantilla reutilizable#
Para probar una garantía de conservación ante fallos:
- crea un estado válido mediante la API pública;
- configura la dependencia para que falle;
- ejecuta la operación que podría reemplazar o destruir el estado;
- comprueba tanto el error como el estado que queda visible.
La misma forma sirve para una configuración que no debe reemplazarse si es inválida, una sincronización que no debe borrar la última copia útil o una renovación de credenciales que no debe eliminar las vigentes antes de obtener otras.
La pregunta decisiva para el test es: ¿qué implementación incorrecta plausible deja de pasar? Aquí la respuesta es concreta: la que borra el valor anterior antes de saber si puede reemplazarlo.
El fundamento de testeabilidad explica por qué una garantía observable permite exigir el comportamiento sin congelar el recorrido interno.
No pruebes solo que la operación falló; prueba qué promesa sigue en pie después del fallo.