flowchart LR
subgraph Tu código
H[Handler] --> S[Servicio] --> A["notifyExternal()<br/>← LA FRONTERA"]
end
A -->|prod| EXT[Slack real]
A -->|test| M["fake: captura args,<br/>devuelve ok o error a elección"]
38 29. Necesitamos testear lo que depende de terceros
La API externa no puede recibir 500 llamadas en cada test run
Tu servicio notifica a Slack — ¿cada npm test va a pegarle a Slack de verdad? No: se reemplaza la dependencia en la frontera correcta — tu adapter notifyExternal del cap. 19, no la librería HTTP. Mockear bien es responder: “¿esto verifica que yo llamé bien, o que ellos funcionan?”.
38.1 El problema
El test del registro crea el usuario… y envía el email real, llama a Slack real, cobra en Stripe real. Tests lentos, que fallan cuando el tercero se cae, que gastan rate limit, y que envían correos de mentira a usuarios de verdad. Pero el extremo opuesto es igual de inútil: mockear tan adentro que el test solo verifica que el mock funciona.
Un mock es un doble de pruebas: finge ser el servicio externo (Stripe, SMTP) para que el test no cobre ni mande emails. Se mockea en la frontera (tu adaptador), nunca la BD — fingir la BD es mentir el test.
Un registry es el almacén de imágenes (Docker Hub, ECR, Artifact Registry): el CI empuja nexus:v42, el servidor la jala. Es el intermediario entre “se construyó” y “está corriendo”.
38.2 Cómo lo resuelve un equipo
Un equipo serio mockea en la frontera que él diseñó: el adapter con nombre de negocio (notifyExternal, chargeCard) — no fetch, no httpx, no net/http. Razón: lo que quieres verificar es tu contrato (“¿llamaste con el texto correcto? ¿degradaste si falló?”), no la librería HTTP de alguien. Y la regla que evita el teatro: un mock que siempre responde bien solo prueba el día que todo salió bien — el camino que importa es el del fallo.
38.3 Conceptos nuevos
- TS
vi.mock· Pymonkeypatch/unittest.mock· Go interfaces inyectadas - U La frontera del mock: qué sustituir y qué no — Go fuerza DI por diseño; TS/Py la simulan
- U Cobertura: qué significa 40%/80% y por qué 100% puede ser peor que 70% bien elegido
- Ops Tests como requisito de merge: cobertura mínima en CI
38.4 La explicación visual
El mock vive donde tu diseño ya separó la integración (cap. 19) — por eso el adapter con nombre de negocio era importante: hoy es la costura donde cosen los tests.
38.5 Implementación
Sustituir el adapter en los tres mecanismos:
vi.mock("./notify", () => ({
notifyExternal: vi.fn(), // the adapter, not fetch
}));
it("degrades when slack is down", async () => {
vi.mocked(notifyExternal).mockRejectedValue(new ExternalServiceError("down"));
const res = await request(app).post("/tasks").set("Cookie", token)
.send({ title: "x" });
expect(res.status).toBe(201); // external failure ≠ your failure
expect(notifyExternal).toHaveBeenCalledWith(expect.stringContaining("x"));
});def test_degrades_when_slack_down(client, monkeypatch):
calls = []
def fake_notify(text): calls.append(text); raise ExternalServiceError("down")
monkeypatch.setattr("app.services.notify.notify_external", fake_notify)
res = client.post("/tasks", json={"title": "x"}, headers=auth_headers)
assert res.status_code == 201 # external failure ≠ your failure
assert calls and "x" in calls[0]
# FastAPI alternative: app.dependency_overrides[notify_dep] = lambda: fake_notify// the service takes the dependency as an interface — the seam is in the signature
type Notifier interface{ Notify(ctx context.Context, text string) error }
type fakeNotifier struct{ err error; got []string }
func (f *fakeNotifier) Notify(_ context.Context, text string) error {
f.got = append(f.got, text); return f.err
}
func TestDegradesWhenSlackDown(t *testing.T) {
fn := &fakeNotifier{err: errors.New("down")}
srv := httptest.NewServer(newRouter(testDeps{notifier: fn}))
res := postTask(t, srv, token, `{"title":"x"}`)
if res.StatusCode != 201 { t.Fatalf("external failure leaked: %d", res.StatusCode) }
if len(fn.got) != 1 { t.Fatal("notify not called") }
}Diseña los tests de mi integración con [Slack/email/pagos] en
[Express/FastAPI/net-http]. Requisitos: sustituir el adapter
notifyExternal (no fetch/httpx/http.Client) usando
[vi.mock/monkeypatch/dependency_overrides/interfaz inyectada]; cubrir
los tres caminos — éxito (llamó con el payload correcto), fallo
transitorio (reintentó y degradó sin tumbar mi respuesta), y fallo
permanente (error de dominio registrado, usuario afectado según lo
diseñado). Verificar también que NO se llama cuando la regla dice que
no (ej. no notificar si la tarea falló validación).
38.6 ¿Por qué cada stack lo hace así?
Lo único que necesitas llevarte: el mock vive en la frontera de tu diseño — el adapter. Si tienes que mockear fetch o httpx para testear tu lógica, la señal real es que te falta el adapter (cap. 19), no que falta el mock.
- TS:
vi.mockreemplaza el módulo — funciona aunque el servicio hagaimportdirecto; potente pero mágico: el mock reemplaza por nombre de archivo. - Python:
monkeypatchcambia el atributo en runtime; FastAPI además tienedependency_overrides— la DI del cap. 16 convertida en costura de tests oficial. - Go: sin mocks de framework — la interfaz en la firma es la costura; inyectar
fakeNotifieres solo pasar otra implementación. Más explícito: la testabilidad se ve en la firma del servicio.
Cobertura honesta: 40% cubriendo servicios+auth vale más que 90% de getters. 100% suele comprar tests que no matan bugs — el número es un mapa de lo no cubierto, no una nota del examen.
38.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
| Mockear la librería HTTP | “Intercepto fetch” | Mockea tu adapter — si no existe, eso es el hallazgo |
| Mock que siempre devuelve éxito | El camino feliz es el fácil | Test del fallo: 500, timeout, respuesta rara — ahí vive tu lógica |
| Assert sobre el mock, no el efecto | expect(mock).toHaveBeenCalled() y ya |
Ambos: llamó bien y mi sistema respondió según lo diseñado |
| Mockear tu propia capa de servicio | Para “testear el handler” | El handler con servicio mockeado no testea nada real — baja al adapter |
| 100% cobertura como meta | Métrica bonita | Cobertura en reglas y fronteras; getters no matan bugs |
38.8 Buenas prácticas
- Tres caminos por integración: éxito, fallo transitorio, fallo permanente — la degradación elegante del cap. 19 se demuestra aquí, no se espera.
- El fake también graba:
got []string— verificar con qué te llamaron importa tanto como que te llamaron. - Tests de no-llamada: “si la validación falla, NO se notifica” — los efectos colaterales ausentes también son contrato.
- Un test de humo real (opcional, aparte): una llamada verdadera al sandbox del tercero en CI nocturno — el mock dice que llamaste bien; solo el humo dice que la API de ellos no cambió.
38.9 Ejercicio
- ¿Por qué mockear
fetch/httpxes la frontera incorrecta aunque técnicamente funcione? - Tu test mockea
notifyExternaly verifica que se llamó. ¿Qué dos cosas importantes NO está verificando? - En Go no hay
vi.mock— ¿por qué se dice que “la DI es el mock de Go” y qué implica para cómo escribes los servicios?
Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.
38.10 Mini reto
El test de “servicio externo caído → la API responde bien igual”.
Ejercicio.
- Tres razones: (a) testeas la librería, no tu contrato — si el mock de
fetchsimula mal la API real, el test miente; (b) el test se acopla a cómo llamas (URL, headers) — refactorizar el cliente rompe tests que no deberían enterarse; (c) el adapter es tu frontera semántica:notifyExternal("task created")es lo que tu negocio necesita, nofetch(url, opts). - Con qué se llamó — el payload correcto (un typo en el texto pasa el test); (b) qué hizo tu sistema con el resultado — el assert del mock no verifica tu 201, tu 502, ni tu degradación. El mock verifica la llamada; el test debe verificar la respuesta.
- La interfaz en la firma es la costura:
NewService(n Notifier)recibe la implementación — en prod el cliente real, en test el fake. Implica que los servicios declaran sus dependencias como interfaces pequeñas (1-3 métodos) en vez de esconderhttp.Postadentro — la testabilidad se diseña en la firma, no se parchea después.
Mini reto. El test de los ejemplos de arriba: fake que lanza/retorna error → POST /tasks → assert 201 (o el status diseñado) + assert de que el usuario recibe su tarea + assert de que el error quedó registrado. Es el test que demuestra la degradación elegante del cap. 19 — sin él, “degrada bien” es una esperanza.
38.11 Vocabulario técnico del capítulo
Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.
Un objeto es una colección de datos con nombre: {name: "Ana", age: 30} — cada dato es una propiedad (clave → valor). Python los llama dict, Go los arma con struct, TS con objetos/interface.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
Un string es texto entre comillas: "hola". El nombre viene de “cadena de caracteres” — una secuencia de letras. Todo lo que llega de un formulario o una URL llega como string, aunque parezca número.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
Asíncrono = empezar algo sin esperar sentado a que termine: pides la pizza (async) y sigues trabajando; cuando llega, te avisan. Lo opuesto a síncrono (esperar parado). Vital cuando la espera es larga: red, disco, bases de datos.
Un método es una función que vive dentro de un objeto/clase: task.save() — save es un método de task. La diferencia con una función suelta: el método conoce al objeto que lo contiene (this/ self).
La cobertura es el % de tu código que los tests ejecutan. Útil como detector de “esto nadie lo prueba”; inútil como meta — 90% cubierto no significa que los tests afirmen algo correcto.
El runtime es el motor que ejecuta tu código: Node.js es el runtime de JavaScript fuera del navegador; CPython es el de Python; Go compila a binario y el runtime va empaquetado dentro. Es “quien corre” lo que escribiste.
Un framework es un esqueleto de aplicación ya decidido: te da la estructura (rutas, validación, errores) y tú llenas la lógica. Diferencia con librería: la librería la llamas tú; el framework te llama a ti.
Una dependencia es código de terceros que tu proyecto usa: npm install, pip install, go get las traen. Cada una es deuda — ahora funciona, pero hay que mantenerla, actualizarla y confiar en ella.
La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.
Un token es una credencial portable: una cadena que dice quién eres y hasta cuándo. El servidor la emite tras el login; el cliente la presenta en cada request en vez de la contraseña.
Una firma (HMAC) prueba que un mensaje es auténtico y no fue tocado: se calcula con un secreto sobre el contenido. Los webhooks la usan para que verifiques “esto realmente vino de Stripe”.
Un dominio es el nombre que compras (midominio.com) y apuntas a tu servidor por DNS. Sin él, tus usuarios tendrían que memorizar una IP. El HTTPS serio requiere dominio.
Producción (prod) es el entorno real: donde están los usuarios, los datos que importan y las consecuencias. Todo lo demás — local, staging — existe para que los errores ocurran antes de llegar ahí.
Un timeout es la fecha límite de una espera: “si la BD no responde en 5s, falla”. Sin timeout, un servicio caído convierte tu espera en infinita — tu app muere de pie esperando.