El test como consumidor#
Un test funcional representa a un consumidor del módulo. Entra por su API y comprueba resultados, estado o mensajes que pertenecen al contrato.
La caja negra no tiene que abarcar todo el sistema. Puede ser un módulo pequeño siempre que la prueba respete su límite y no convierta la coordinación interna en una promesa pública.
test → API → implementación
└── API consumida → colaboradorLa pregunta principal es qué debe seguir siendo cierto para el consumidor. La elección entre implementación real, doble de prueba o integración viene después.
Qué puede observar una prueba#
Un contrato ofrece tres clases de observación:
| Observación | Ejemplo genérico | |---|---| | Resultado directo | Valor o error devuelto | | Estado accesible por la API | Una consulta posterior refleja el cambio | | Mensaje saliente | Se publica un evento o se solicita un efecto |
Los helpers utilizados, el reparto entre objetos internos y el algoritmo elegido quedan fuera. También quedan fuera el número y el orden de llamadas cuando no cambian el resultado observable.
Una prueba funcional debería aceptar dos implementaciones que produzcan los mismos resultados, el mismo estado visible y los mismos mensajes contractuales.
Ejemplo: conservar un valor ante un fallo#
Consideremos un módulo de caché:
Cache.Refresh(Key) → Value | SourceUnavailable
Cache.Get(Key) → Value | NotFoundCache consume otra API:
Source.Load(Key) → Value | errorQueremos demostrar una sola garantía: si la fuente falla durante una actualización, el valor anterior continúa disponible.
El test ejecuta la implementación real de Cache y prepara un doble de Source que devuelve un fallo. Después observa únicamente la API:
dado un valor almacenado para una clave
cuando Refresh recibe SourceUnavailable
entonces devuelve SourceUnavailable
y Get conserva el valor anteriorLa prueba no necesita saber si la caché escribe primero en una copia, usa un bloqueo o revierte una asignación. Tampoco necesita una fuente remota real, porque el riesgo observado es la reacción del módulo al fallo, no el protocolo de red.
Una prueba diferente debería comprobar el adaptador real si el riesgo estuviera en la serialización, la configuración o el transporte.
Ejemplo en Go#
Suponiendo la API anterior, el caso puede escribirse sin observar ninguna llamada interna.
import (
"errors"
"testing"
)
type sourceStub struct{ err error }
func (s sourceStub) Load(string) (string, error) {
return "", s.err
}
func TestRefreshKeepsPreviousValue(t *testing.T) {
cache := NewCache(sourceStub{err: ErrSourceUnavailable})
cache.Put("key", "previous")
_, err := cache.Refresh("key")
if !errors.Is(err, ErrSourceUnavailable) {
t.Fatalf("expected source error, got %v", err)
}
got, _ := cache.Get("key")
if got != "previous" {
t.Fatalf("expected previous value, got %q", got)
}
}El test controla una entrada indirecta mediante un stub y comprueba resultado y estado. No exige cuántos helpers debe ejecutar la caché.
Hablar con el vocabulario habitual#
Doble de prueba es el término general para cualquier sustitución usada durante una prueba. Dentro de esa familia conviene mantener los significados conocidos, en lugar de redefinir fake para abarcarlo todo.
| Término | Uso | |---|---| | Implementación real | La misma implementación usada fuera del test | | Fake | Implementación funcional simplificada, como un almacenamiento en memoria | | Stub | Devuelve respuestas preparadas para controlar una entrada indirecta | | Spy | Registra mensajes para poder observarlos después | | Mock | Declara y verifica expectativas sobre interacciones |
La terminología procede de la taxonomía recogida por Gerard Meszaros y resumida por Martin Fowler en Test Double. Saber el nombre ayuda, pero la decisión importante sigue siendo qué comportamiento sustituye el doble y qué riesgo deja sin cubrir.
En el ejemplo anterior basta un stub de Source: prepara el fallo que el caso necesita. Un fake funcional sería útil si muchos tests necesitaran una fuente en memoria con reglas estables. Un mock solo tendría sentido si una interacción concreta formara parte del contrato.
Elegir colaboradores#
La implementación real ofrece la mayor fidelidad y es la primera opción cuando resulta rápida, determinista, hermética, segura y fácil de construir. La guía de Software Engineering at Google sobre test doubles propone el mismo punto de partida pragmático.
| Situación | Elección habitual | |---|---| | Colaborador rápido y determinista | Implementación real | | Muchos casos necesitan semántica estable sin infraestructura | Fake funcional | | Un caso necesita una respuesta excepcional | Stub local | | El contrato incluye un mensaje saliente | Spy o mock | | El riesgo depende de protocolo, transacción o configuración | Integración con el adaptador real |
No es necesario sustituir valores, entidades o funciones puras solo porque colaboren en el caso. Tampoco hace falta levantar infraestructura real para demostrar una regla que no depende de ella.
Cuándo observar interacciones#
Un mensaje saliente puede formar parte del contrato. En ese caso un spy o un mock permite observar su contenido, cantidad u orden.
La expectativa se limita al contenido, la cantidad o el orden solo cuando esa diferencia cambia el efecto para otro consumidor.
Comprobar que se llamó a un mapper, una query concreta o un helper privado congela la implementación. Comprobar que se publicó un mensaje requerido protege el contrato.
Una interacción solo demuestra que el mensaje se intentó enviar. No demuestra que el receptor real lo acepte. Cuando esa compatibilidad es el riesgo, hace falta una prueba del adaptador o una integración.
Qué debe conservar un fake#
Un fake funcional implementa un subconjunto declarado del contrato. Para los casos soportados acepta las mismas entradas y conserva resultados, errores y estado. Debe ser determinista, aislar cada test y hacer visibles las capacidades o propiedades que omite.
No necesita copiar la infraestructura. Una fuente en memoria puede reproducir lectura, ausencia y versiones sin simular red o latencia. Si el caso trata precisamente de esas propiedades omitidas, el fake deja de ser una prueba suficiente.
Un fake compartido acumula responsabilidad y merece tests propios. Cuando sea viable, una misma suite de contrato puede ejecutarse contra el fake y el adaptador real para detectar divergencias.
Método de revisión#
Para diseñar una prueba:
- Nombra la garantía y la observación pública que la demuestra.
- Usa colaboradores reales mientras sean prácticos; introduce el doble mínimo cuando necesites controlar u observar el caso.
- Elimina afirmaciones internas y comprueba que otra implementación correcta también pasaría. La fidelidad que falte determina la integración necesaria.
Primero decide qué debe observarse; después elige con qué implementaciones puede demostrarse.