Webhook Testing Strategies for Production
Why Webhook Testing Is Different
Testing webhook handlers presents unique challenges compared to testing regular API endpoints. Your handler is the server receiving requests, not the client making them. The request format, timing, and content are controlled by an external provider, not by your code. And the behavior you need to test often involves side effects — database changes, email sends, API calls — triggered by incoming events.
Traditional API testing is client-centric: you craft a request, send it, and verify the response. Webhook testing is server-centric: you craft an incoming request that mimics what a provider sends, deliver it to your handler, and verify the side effects are correct.
A comprehensive webhook testing strategy includes unit tests (verify handler logic with mocked payloads), integration tests (verify end-to-end processing with real databases and services), and development testing (verify your handler works with actual webhook deliveries from the provider).
Unit Testing Webhook Handlers
Unit tests verify your handler logic in isolation with mocked dependencies. Use real webhook payloads as test fixtures — capture them from the provider's documentation, their webhook simulator, or from the ReqPour dashboard during development.
describe('Stripe webhook handler', () => {
const paymentSucceededPayload = {
id: 'evt_test_123',
type: 'payment_intent.succeeded',
data: {
object: {
id: 'pi_test_456',
amount: 2500,
currency: 'usd',
customer: 'cus_test_789',
}
}
};it('creates order on payment_intent.succeeded', async () => { const mockDb = { orders: { create: jest.fn() } }; await handleStripeEvent(paymentSucceededPayload, mockDb); expect(mockDb.orders.create).toHaveBeenCalledWith( expect.objectContaining({ amount: 2500, currency: 'usd' }) ); });
it('skips duplicate events', async () => { const mockDb = { processedEvents: { findUnique: jest.fn().mockResolvedValue({ eventId: 'evt_test_123' }) }, orders: { create: jest.fn() } }; await handleStripeEvent(paymentSucceededPayload, mockDb); expect(mockDb.orders.create).not.toHaveBeenCalled(); }); }); ```
Test each event type your handler processes, plus edge cases: unknown event types (should be ignored gracefully), malformed payloads, missing fields, and duplicate events.
Integration Testing with Real Services
Integration tests verify that your handler works with real databases, queues, and external services. Use a test database and fire real-format webhook payloads at your handler through HTTP.
describe('Webhook integration', () => {
beforeEach(async () => {
await db.processedEvents.deleteMany();
await db.orders.deleteMany();
});it('processes payment and creates order', async () => { const response = await fetch('http://localhost:3000/api/webhooks/stripe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(paymentSucceededPayload), });
expect(response.status).toBe(200);
const order = await db.orders.findFirst({ where: { stripePaymentIntentId: 'pi_test_456' } }); expect(order).toBeTruthy(); expect(order.amount).toBe(2500); }); }); ```
For integration tests, you may want to skip signature verification (using an environment variable flag) so you can send test payloads without computing valid signatures. Alternatively, compute valid signatures in your test using the same secret key.
End-to-End Testing with ReqPour
The most realistic testing approach uses actual webhooks from the provider. During development, use ReqPour to receive real webhooks, relay them to your local server, and verify the complete flow works.
This catches issues that unit and integration tests miss: unexpected payload formats from new API versions, timing issues with rapid consecutive events, and encoding mismatches in signature verification. When you find an issue, capture the problematic payload in the ReqPour dashboard and add it to your test fixtures.
ReqPour's replay feature turns captured webhooks into a repeatable test suite. Capture representative events during development, document which events you captured, and replay them whenever you change your handler code. This gives you confidence that changes do not break existing functionality.
For continuous integration, export captured webhook payloads from your development testing sessions and use them as fixtures in your automated test suite. This bridges the gap between realistic end-to-end testing (using ReqPour during development) and automated testing (running in CI).
Testing in Staging and Production
Before deploying webhook handler changes to production, test them in a staging environment with real (sandbox) webhook providers. Point the provider's sandbox webhooks at your staging server and verify the complete flow with real payloads. Most providers (Stripe, PayPal, Shopify) offer sandbox or test mode specifically for this purpose.
In production, monitor your webhook handler's health metrics: response status codes, processing latency, error rates, and queue depths (if using async processing). Set up alerts for elevated error rates or increased latency, which could indicate a new event format, a provider change, or a bug in your handler.
Keep a log of processed webhook events with their event IDs and timestamps. This audit trail is invaluable for debugging production issues and for reconciliation — comparing your processed events with the provider's delivery logs to identify missed events.
After deploying handler changes, watch the next few webhook deliveries closely. Many providers show delivery status in their dashboard, and you can compare this with your own processing logs to verify everything is working correctly.
Related
Get started with ReqPour
Catch, inspect, and relay webhooks to localhost. Free to start, $3/mo for Pro.