Tu prueba de webhook pasa una vez. Aun así, el handler puede aceptar un cuerpo modificado, procesar dos veces el mismo evento o esconder un fallo que hará que el proveedor vuelva a intentarlo.
Las pruebas útiles empiezan en el límite de la entrega. Envía los bytes exactos que se firmaron, comprueba el encabezado, valida el evento, confirma la respuesta HTTP y mide el efecto. Después repite el evento, fuerza fallos y cambia el orden de las entregas.
Esto es distinto de elegir un tipo de prueba de software. Aquí la pregunta no es si una prueba es unitaria o de integración. Es qué comportamiento del webhook demuestra cada escenario. Los ejemplos de código son ilustrativos y no se ejecutaron contra un proveedor real.

Respuesta corta
- Conserva el cuerpo original antes de
JSON.parsey firma los mismos bytes que firma el proveedor.- Prueba firmas ausentes, inválidas y caducadas, además del camino válido.
- Envía dos veces el mismo evento, también en paralelo, y confirma un solo efecto de negocio.
- Simula timeouts, respuestas
5xx, reentregas y eventos fuera de orden sin esperar al reloj real.
¿Qué debe demostrar una prueba de webhook?
Una prueba de webhook debe responder cinco preguntas: ¿la solicitud es auténtica?, ¿el payload tiene la forma esperada?, ¿el endpoint reconoce la entrega?, ¿la repetición es segura? y ¿el efecto correcto ocurre una sola vez? Un 200 aislado responde solo una parte de la tercera pregunta.
La especificación de Standard Webhooks, publicada por el proyecto Standard Webhooks, trata la firma, la marca de tiempo y el identificador de entrega como metadatos separados del cuerpo. El identificador puede funcionar como clave de idempotencia y la marca de tiempo ayuda a limitar el replay. Tu proveedor puede usar nombres y fórmulas distintos, así que su contrato sigue siendo la referencia.
| Pregunta | Escenario mínimo | Evidencia que conviene guardar |
|---|---|---|
| ¿Se autenticó el origen? | firma válida e inválida | estado y motivo del rechazo |
| ¿Se interpretó bien el cuerpo? | evento válido, inválido y desconocido | error de validación o tipo aceptado |
| ¿Puede el proveedor dejar de reintentar? | éxito, 4xx y 5xx |
respuesta y estado de la entrega |
| ¿Es seguro un duplicado? | mismo ID dos veces | un efecto y dos respuestas aceptables |
| ¿Importa el orden? | eventos relacionados en órdenes distintas | estado final o reconciliación explícita |
Esta matriz convierte una integración en un contrato que se puede revisar. También evita que la suite use la palabra "webhook" para ocultar una llamada directa a una función interna. La prueba debe cruzar el mismo límite que cruzará el proveedor.
El resultado más útil va más allá de passed. Combina la respuesta, el evento aceptado, el ID de entrega, el estado persistido y los efectos externos. Cuando aparece un duplicado, esa pista muestra si el handler lo rechazó pronto o si ejecutó el trabajo antes de descubrir que ya existía.
¿Cómo crear un fixture determinista?
Construye el fixture como una solicitud HTTP completa, no como un objeto de JavaScript que salta la serialización. El cuerpo original, los encabezados, el secreto de prueba, el ID del evento y la marca de tiempo deben poder controlarse. Así la suite puede repetir exactamente la misma entrega.
Node.js, "Crypto" proporciona createHmac para calcular un HMAC. El algoritmo y el texto firmado de abajo son un contrato genérico. Stripe, GitHub y otros proveedores definen sus propios encabezados, prefijos y reglas de tiempo. Copia la fórmula del proveedor, no este ejemplo por costumbre.
import { createHmac } from "node:crypto";
type WebhookRequest = {
body: string;
headers: Record<string, string>;
};
export function makeWebhookRequest(
body: string,
secret: string,
eventId = "evt_test_123",
timestamp = 1_758_000_000,
): WebhookRequest {
const signedValue = `${timestamp}.${body}`;
const signature = createHmac("sha256", secret)
.update(signedValue)
.digest("hex");
return {
body,
headers: {
"content-type": "application/json",
"x-webhook-id": eventId,
"x-webhook-timestamp": String(timestamp),
"x-webhook-signature": signature,
},
};
}
Usa una cadena fija para el cuerpo durante la prueba. Si el handler analiza y después vuelve a serializar el JSON antes de verificarlo, las diferencias de espacios, orden de propiedades o escapes pueden invalidar una firma correcta. Mantén el secreto en el fixture, nunca en el texto del error ni en los logs.
Si el framework de la aplicación ya consumió el cuerpo, añade una prueba que demuestre esa configuración. La verificación necesita los bytes originales. La documentación de Stripe, en "Receive Stripe events in your webhook endpoint", también indica que la firma debe verificarse contra el cuerpo recibido y que cada endpoint tiene su propio secreto.
¿Qué casos negativos deben fallar?
Empieza por los casos que puede producir un atacante o un fallo de transporte: encabezado ausente, secreto incorrecto, cuerpo modificado después de firmarlo, marca de tiempo fuera de la ventana y JSON inválido. La prueba del camino feliz solo sirve después de definir la respuesta para estos límites.
Stripe documenta la marca de tiempo y la firma como protección contra replay en "Receive Stripe events in your webhook endpoint". La misma documentación explica que un nuevo intento puede recibir una firma y una marca de tiempo nuevas. Por eso la suite debe distinguir un reintento legítimo del mismo evento de un replay antiguo o una solicitud falsificada.
import { describe, expect, it } from "vitest";
describe("webhook verification", () => {
it("accepts the exact signed body", async () => {
const request = makeWebhookRequest(
'{"type":"order.created","id":"evt_test_123"}',
"test-secret",
);
const result = await handleWebhook(request, "test-secret");
expect(result.status).toBe(202);
});
it.each([
["missing signature", (request: WebhookRequest) => {
delete request.headers["x-webhook-signature"];
}],
["changed body", (request: WebhookRequest) => {
request.body = '{"type":"order.cancelled","id":"evt_test_123"}';
}],
["wrong secret", (request: WebhookRequest) => {
request.headers["x-webhook-signature"] = "not-the-signature";
}],
])("rejects %s", async (_name, change) => {
const request = makeWebhookRequest(
'{"type":"order.created","id":"evt_test_123"}',
"test-secret",
);
change(request);
const result = await handleWebhook(request, "test-secret");
expect(result.status).toBe(401);
});
});
El fragmento llama a handleWebhook como límite del sistema y es deliberadamente ilustrativo. Una suite real debe usar el adaptador HTTP de la aplicación, el verificador del proveedor y una respuesta que no revele el secreto. Prueba también un secreto rotado, un content type inesperado, campos obligatorios ausentes y un evento desconocido.
¿Cómo demostrar que los duplicados no repiten el efecto?
Envía dos veces el mismo evento con el mismo identificador y confirma que la primera entrega crea el efecto esperado, mientras la segunda responde de forma segura sin ejecutar de nuevo la lógica de negocio. Después envía dos llamadas al mismo tiempo. Ese caso encuentra una carrera que una prueba secuencial no detecta.
No uses solo un Set en memoria para demostrar idempotencia. Desaparece cuando el proceso se reinicia y no coordina dos réplicas. La especificación de Standard Webhooks recomienda usar el ID de entrega como clave de idempotencia. La opción concreta puede ser una restricción única en la base de datos, una operación atómica de cola u otro almacenamiento durable.
it("processes the same delivery once", async () => {
const request = makeWebhookRequest(
'{"type":"order.created","id":"evt_test_123"}',
"test-secret",
);
const [first, second] = await Promise.all([
handleWebhook(request, "test-secret"),
handleWebhook(request, "test-secret"),
]);
expect([first.status, second.status].sort()).toEqual([202, 202]);
expect(await countCreatedOrders("evt_test_123")).toBe(1);
});
La prueba debe observar el efecto que importa. Contar llamadas a una función simulada no basta cuando el fallo está en una escritura sin restricción, una cola o un pago que se activa después. Si el handler acepta la entrega y encola el trabajo, confirma también que el mensaje deduplicado aparece una sola vez.
Este artículo no afirma experiencia con clientes ni un benchmark. Su regla práctica es un límite de verificación: si la prueba no puede decir cuántos efectos se produjeron para un ID repetido, todavía no demuestra idempotencia.
¿Cómo probar reintentos sin esperar al reloj real?
Haz que el adaptador externo falle de forma controlada y avanza un reloj falso entre los intentos. La suite debe comprobar el número de llamadas, los retrasos elegidos, el estado final y el efecto producido. No uses sleep para esperar un reintento que tarda minutos.
Stripe documenta que un endpoint que no responde con éxito puede recibir más intentos y recomienda responder rápido antes de ejecutar lógica compleja en "Receive Stripe events in your webhook endpoint". La regla exacta depende del proveedor, pero la prueba debe dejar claro qué respuesta provoca una reentrega en el contrato elegido.
| Escenario | Respuesta del handler | Qué comprobar |
|---|---|---|
| procesamiento terminado | 200 o 202 |
un efecto y ningún reintento esperado |
| fallo temporal antes del efecto | 5xx |
el proveedor o simulador vuelve a intentar |
| duplicado ya persistido | 200 o 202 |
ningún efecto nuevo |
| payload inválido | 4xx |
la entrega no entra en la lógica de negocio |
| fallo después del efecto | contrato explícito | reconciliación o deduplicación evita repetirlo |
Para el límite del reloj, consulta cómo probar reintentos en JavaScript sin esperar. El webhook añade otra pregunta: ¿el reintento repite solo la entrega o también una llamada externa que ya tuvo éxito? Da a ese límite su propia prueba.
¿Qué ocurre si los eventos llegan fuera de orden?
Prueba el orden cuando el estado de un recurso depende de más de un evento. Envía una actualización antes del evento que crea el recurso y después repite la secuencia normal. El sistema debe rechazar, guardar, reconciliar o aplicar los eventos según una regla explícita. Ignorar la diferencia no es una política.
GitHub incluye las entregas fuera de orden entre los problemas de webhooks en "Troubleshooting webhooks". También recomienda inspeccionar la entrega y la respuesta recibida por el servidor. En tu suite, conserva el ID y el tipo de cada evento para que el fallo muestre la secuencia recibida.
Una forma sencilla de probarlo es modelar el estado final esperado y comparar la consecuencia, no solo el orden de las llamadas. Si el dominio necesita monotonicidad, el evento debe llevar una versión o una marca de tiempo que el almacenamiento pueda comparar. Si el orden no está garantizado, el consumidor debe consultar el estado actual o programar una reconciliación.
¿Cómo separar las pruebas locales de las del proveedor?
Usa tres capas. La primera prueba el verificador, el parser y la regla de deduplicación con fixtures locales. La segunda cruza el endpoint HTTP y comprueba el estado, el almacenamiento y la cola. La tercera usa el sandbox, CLI, replay o entrega de prueba del proveedor para confirmar que el adaptador local sigue su protocolo.
La documentación de GitHub sobre probar webhooks describe cómo reenviar entregas a un servidor local e inspeccionar lo que se envió y recibió. Es útil para comprobar el formato real, pero no sustituye las pruebas deterministas que deberían ejecutarse en cada pull request.
Tampoco conviertas un mock de red en una prueba de integración. El artículo sobre mockear APIs en Playwright sin falsos positivos explica la diferencia entre controlar el estado de la interfaz y comprobar el contrato real. En un webhook, el mock puede producir casos raros y una llamada al sandbox confirma encabezados, bytes y comportamiento del proveedor.
Guarda una evidencia pequeña por escenario: entrada redactada, ID, estado, motivo de la decisión, estado final y cantidad de efectos. No almacenes secretos ni payloads con datos personales sin una regla de retención. El objetivo es reproducir el fallo, no crear una segunda base de producción dentro de los artefactos del CI.
Checklist del webhook antes del CI
Usa este orden al revisar un handler nuevo o cambiar una integración existente:
- Fija un payload representativo y conserva el cuerpo original usado para firmarlo.
- Prueba firmas válidas, ausentes, modificadas, caducadas y con secreto incorrecto.
- Valida el schema antes de llamar a la lógica de negocio.
- Confirma el comportamiento para eventos desconocidos y payloads inválidos.
- Entrega el mismo ID dos veces y confirma un efecto.
- Entrega dos copias en paralelo y ejerce la deduplicación atómica.
- Fuerza un timeout y un
5xxantes y después del efecto para descubrir el contrato de reintento. - Reproduce eventos fuera de orden y registra la decisión de reconciliación.
- Ejecuta una prueba en el sandbox o mediante el mecanismo oficial de replay del proveedor.
- Haz que el CI publique solo evidencia redactada y falle cuando una promesa del contrato deje de estar demostrada.
Si las pruebas de tiempo siguen siendo frágiles, controla fechas y timers en JavaScript antes de aumentar los reintentos del runner. Repetir una prueba puede revelar flakiness, pero no corrige una deduplicación ausente ni una firma calculada sobre el cuerpo equivocado.
Preguntas frecuentes
¿Una prueba que recibe 200 demuestra que el webhook funciona?
No. 200 demuestra solo la respuesta de ese escenario. También debes demostrar firmas, validación, duplicados, fallos y el efecto de negocio. Una prueba útil registra el estado persistido y la cantidad de efectos, no solo el código HTTP.
¿Debo probar contra la API real del proveedor?
Sí, en una capa separada. Un sandbox, CLI o replay comprueba el protocolo real, pero es más lento y depende de credenciales, red y disponibilidad. Los fixtures locales deben cubrir las combinaciones de error en CI; el proveedor debe confirmar el adaptador y el formato de entrega.
¿Puedo usar un mock para validar la firma?
Puedes simular la fuente de entrega para probar tu regla, pero el mock debe generar los mismos bytes y encabezados del contrato. Si llama a una función que ya devuelve "firma válida", la prueba no demuestra nada sobre la integración. Conserva una muestra firmada y una prueba negativa.
¿Cuál es la mejor clave para deduplicar?
Usa el identificador estable que el proveedor define para una entrega o un evento. No derives la clave solo del texto del payload si dos entregas legítimas pueden tener el mismo contenido. Persiste la decisión de forma atómica y confirma en una prueba que dos llamadas concurrentes producen un efecto.
Conclusión
Un webhook confiable no es un endpoint que respondió 200 una vez. Es un límite probado con bytes conservados, firma verificada, payload validado, respuesta clara, deduplicación durable, reintentos controlados y una regla explícita para el orden de los eventos.
Empieza con fixtures locales y casos negativos. Después cruza el endpoint, simula concurrencia y confirma el adaptador en el sandbox o el replay oficial del proveedor. Cuando cada escenario deja evidencia pequeña y verificable, el CI puede proteger la integración sin fingir que una prueba feliz representa internet.
Fuentes consultadas
- GitHub, "Testing webhooks" y "Troubleshooting webhooks", consultado el 08/09/2026.
- Stripe, "Receive Stripe events in your webhook endpoint", consultado el 08/09/2026.
- Standard Webhooks, "Standard Webhooks specification", consultado el 08/09/2026.
- Node.js, "Crypto", consultado el 08/09/2026.
- Vitest, "Mocking Timers", consultado el 08/09/2026.