Overview
Testing email webhook integrations requires more than a unit test for your handler function. You need to test the full pipeline: from email send to SMTP receipt, MIME parsing, webhook delivery, signature verification, and handler processing. Each step can fail independently.
A thorough testing strategy includes:
- Unit tests: Test your signature verification and payload parsing functions in isolation
- Local integration tests: Run your webhook handler locally with a tunnel and send real emails to a test JsonHook address
- Synthetic payload tests: Use the JsonHook API to deliver a synthetic payload to your endpoint without sending a real email
- Production smoke tests: After deployment, send a canary email through the production pipeline to confirm end-to-end health
Prerequisites
Testing infrastructure you need:
- A separate JsonHook address for testing (do not use production addresses for test emails)
- A tunnel tool for local testing: ngrok, Cloudflare Tunnel, or Tunnelmole
- An email client or
curlwith SMTP access for sending test emails - Your test framework of choice (Jest, Pytest, Go testing, RSpec, etc.) for unit tests
Test Your Email Webhook Integration
Synthetic payloads, delivery logs, and replay — all included. Start free.
Get Free API KeyStep-by-Step Instructions
Build a comprehensive testing workflow:
- Unit test signature verification:
test("verifySignature accepts valid signature", () => { const body = Buffer.from('{"email":{"from":"[email protected]"}}'); const secret = "test_secret"; const sig = crypto.createHmac("sha256", secret).update(body).digest("hex"); expect(verifySignature(body, secret, sig)).toBe(true); }); test("verifySignature rejects tampered body", () => { const body = Buffer.from('{"email":{"from":"[email protected]"}}'); const sig = "invalid_signature"; expect(verifySignature(body, "secret", sig)).toBe(false); }); - Set up local webhook testing:
Create a test JsonHook address pointing at the ngrok URL.ngrok http 3000 # Exposes https://abc123.ngrok.io - Use the synthetic payload API to test without sending a real email:
This delivers a realistic synthetic payload to your webhook.POST https://api.jsonhook.com/v1/addresses/{id}/test-delivery Authorization: Bearer YOUR_API_KEY - Send real test emails covering edge cases: HTML-only, with attachments, with UTF-8 subjects, from addresses with display names.
- Check delivery logs after each test to confirm delivery success and response time.
Code Example
Unit tests for a JsonHook webhook handler in Jest/TypeScript:
import crypto from "crypto";
import { verifySignature, parseEmailPayload } from "./webhook-handler";
const TEST_SECRET = "test_secret_abc123";
function makeSignedRequest(body: object) {
const raw = Buffer.from(JSON.stringify(body));
const sig = crypto
.createHmac("sha256", TEST_SECRET)
.update(raw)
.digest("hex");
return { raw, sig };
}
describe("Signature verification", () => {
it("accepts valid signature", () => {
const { raw, sig } = makeSignedRequest({ test: true });
expect(verifySignature(raw, TEST_SECRET, sig)).toBe(true);
});
it("rejects invalid signature", () => {
const { raw } = makeSignedRequest({ test: true });
expect(verifySignature(raw, TEST_SECRET, "bad_sig")).toBe(false);
});
});
describe("Payload parsing", () => {
it("extracts from, subject, and textBody", () => {
const payload = {
email: { from: "[email protected]", subject: "Hello", textBody: "World", attachments: [] },
deliveryId: "dlv_test",
};
const result = parseEmailPayload(payload);
expect(result.from).toBe("[email protected]");
expect(result.subject).toBe("Hello");
});
it("handles null textBody gracefully", () => {
const payload = {
email: { from: "[email protected]", subject: "Hi", textBody: null, htmlBody: "<p>Hi</p>", attachments: [] },
deliveryId: "dlv_test2",
};
const result = parseEmailPayload(payload);
expect(result.body).toBe("<p>Hi</p>");
});
});Common Pitfalls
Testing pitfalls to avoid:
- Only testing the happy path. Test edge cases: null textBody, HTML-only emails, emails with no subject, very large bodies, emails from addresses with display names and special characters.
- Testing with a fixed payload structure. Real email payloads vary. Test with actual emails sent from Gmail, Outlook, and automated sending tools — each produces slightly different MIME structures that affect the parsed JSON.
- Not testing signature verification failures. Ensure your handler returns 401 for invalid signatures and does NOT process the payload. A handler that logs an error but returns 200 for invalid signatures is a security hole.
- Sharing test and production addresses. Always use separate JsonHook addresses for testing to avoid test emails polluting your production logs and triggering production side effects.
- Not testing retry behavior. Simulate a retry by having your handler return 500 on the first call and 200 on the second. Verify idempotency — the email should be processed exactly once.