API-DD

Capítulo del manifiesto

Fundamento 5: Testeabilidad

Verificar el contrato desde fuera sin fijar el recorrido interno.

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.

text
test → API → implementación
                  └── API consumida → colaborador

La 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ónEjemplo genérico
Resultado directoValor o error devuelto
Estado accesible por la APIUna consulta posterior refleja el cambio
Mensaje salienteSe 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é:

text
Cache.Refresh(Key) → Value | SourceUnavailable
Cache.Get(Key)     → Value | NotFound

Cache consume otra API:

text
Source.Load(Key) → Value | error

Queremos 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:

text
dado un valor almacenado para una clave
cuando Refresh recibe SourceUnavailable
entonces devuelve SourceUnavailable
y Get conserva el valor anterior

La 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.

go
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érminoUso
Implementación realLa misma implementación usada fuera del test
FakeImplementación funcional simplificada, como un almacenamiento en memoria
StubDevuelve respuestas preparadas para controlar una entrada indirecta
SpyRegistra mensajes para poder observarlos después
MockDeclara 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ónElección habitual
Colaborador rápido y deterministaImplementación real
Muchos casos necesitan semántica estable sin infraestructuraFake funcional
Un caso necesita una respuesta excepcionalStub local
El contrato incluye un mensaje salienteSpy o mock
El riesgo depende de protocolo, transacción o configuraciónIntegració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:

  1. Nombra la garantía y la observación pública que la demuestra.
  2. Usa colaboradores reales mientras sean prácticos; introduce el doble mínimo cuando necesites controlar u observar el caso.
  3. 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.