Unit Testing Paystack Integrations
Unit test three main areas in a Paystack integration: (1) webhook signature validation — does your HMAC check correctly accept valid signatures and reject invalid ones? (2) business logic — does your order fulfillment function correctly update the DB given a successful charge event? (3) idempotency — does your handler skip processing if the reference was already processed? Mock the Paystack API and the database in these tests.
What to Unit Test in a Paystack Integration
Three areas worth unit testing:
- Webhook signature validation — the HMAC SHA512 check that prevents fake webhooks
- Event handling logic — what happens when charge.success fires (fulfill), charge.failed fires (mark failed), subscription.disable fires (cancel)
- Idempotency — duplicate events for the same reference must not cause double fulfillment
What you do not need to unit test: Paystack's own API behavior, the correctness of the transaction data Paystack returns, or network-level reliability.
Unit Test Examples in Jest
// webhook-handler.test.js
const crypto = require('crypto');
const { handleWebhook } = require('./webhook-handler');
const SECRET = 'test_secret_key';
function makeSignature(body) {
return crypto.createHmac('sha512', SECRET).update(body).digest('hex');
}
describe('Webhook signature validation', () => {
it('accepts a request with valid signature', async () => {
var body = JSON.stringify({ event: 'charge.success', data: { reference: 'ref_001', amount: 100000, status: 'success', metadata: { order_id: '123' } } });
var sig = makeSignature(body);
var result = await handleWebhook(body, sig, SECRET);
expect(result.status).toBe(200);
});
it('rejects a request with wrong signature', async () => {
var body = JSON.stringify({ event: 'charge.success', data: {} });
var result = await handleWebhook(body, 'wrong_sig', SECRET);
expect(result.status).toBe(401);
});
});
describe('Charge fulfillment', () => {
it('fulfills order on charge.success', async () => {
var mockDb = { orders: { update: jest.fn() } };
var event = { event: 'charge.success', data: { reference: 'ref_002', amount: 100000, status: 'success', metadata: { order_id: '456' } } };
await fulfillOrder(event.data, mockDb);
expect(mockDb.orders.update).toHaveBeenCalledWith({ id: '456', status: 'paid', paystack_ref: 'ref_002' });
});
it('does not fulfill on charge.failed', async () => {
var mockDb = { orders: { update: jest.fn() } };
var event = { event: 'charge.failed', data: { reference: 'ref_003', metadata: { order_id: '789' } } };
await handleFailedCharge(event.data, mockDb);
expect(mockDb.orders.update).toHaveBeenCalledWith({ id: '789', status: 'payment_failed' });
// Crucially: fulfillOrder should NOT have been called
});
});
describe('Idempotency', () => {
it('skips fulfillment if reference already processed', async () => {
var mockDb = {
webhookEvents: { findOne: jest.fn().mockResolvedValue({ id: 1 }), create: jest.fn() },
orders: { update: jest.fn() },
};
var event = { event: 'charge.success', data: { reference: 'ref_001', metadata: { order_id: '123' } } };
await handleWithIdempotency(event, mockDb);
expect(mockDb.orders.update).not.toHaveBeenCalled();
});
});
Learn More
This guide is part of the Paystack testing guide.
Key Takeaways
- ✓Unit tests for Paystack code test your logic, not Paystack's API — mock all outbound API calls.
- ✓Test webhook signature validation with both valid and invalid signatures — the invalid case is as important as the valid one.
- ✓Test the fulfillment logic by calling your handler function directly with a mock charge.success event object.
- ✓Test idempotency: call your handler twice with the same reference — the second call should be a no-op.
- ✓Test the failure path: call your handler with a charge.failed event — ensure orders are not fulfilled.
- ✓Keep tests fast: no network calls, no database writes in unit tests — use in-memory mocks for everything.
Frequently Asked Questions
- Should I test the Paystack API response format in unit tests?
- No — you should trust that Paystack returns the format documented in their API reference. Unit tests verify that your code handles those responses correctly. If Paystack changes their API format, your integration tests (which call the real test API) will catch that.
- How do I test webhook handling without a running HTTP server?
- Extract the business logic out of the HTTP handler into a pure function that takes the event object. Test that function directly — no HTTP server needed. The HTTP handler just validates the signature and calls the function. This makes unit testing much simpler.
- What test data should I use for Paystack event mocks?
- Use realistic but fake data: actual reference format (alphanumeric, 12+ chars), amounts in kobo, realistic email addresses. Copy the structure of a real Paystack webhook event from the documentation. The important thing is that the data structure matches what Paystack actually sends.
- How many unit tests should a typical Paystack webhook handler have?
- At minimum: 1 test for valid signature, 1 for invalid signature, 1 for each event type you handle (charge.success, charge.failed, subscription.disable, etc.), and 1 idempotency test. That is typically 5-10 tests for a standard integration. More edge cases (missing metadata, unexpected event types) add value.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon