Integration Testing Paystack Flows End to End
Run integration tests against the Paystack test API using your sk_test key. Flow: (1) call your checkout endpoint which calls POST /transaction/initialize, (2) simulate webhook delivery by generating a real charge.success event payload and POSTing it to your webhook handler with a valid HMAC signature, (3) verify the database reflects the correct final state. Use a local test database and clean up after each test.
Integration Test: Full Payment Flow
// integration/payment-flow.test.js
// Uses real Paystack test API — requires PAYSTACK_SECRET_KEY=sk_test_...
const crypto = require('crypto');
const request = require('supertest');
const app = require('../app');
const db = require('../db');
describe('Paystack payment flow (integration)', () => {
let orderId;
beforeEach(async () => {
// Create a pending order in the test database
var order = await db.orders.create({
email: 'test@example.com',
amount: 10000, // NGN 100
status: 'pending',
});
orderId = order.id;
});
afterEach(async () => {
await db.orders.destroy({ where: { id: orderId } });
});
it('initializes transaction and returns authorization_url', async () => {
var res = await request(app).post('/api/checkout').send({
email: 'test@example.com',
amount: 10000,
order_id: orderId,
});
expect(res.status).toBe(200);
expect(res.body.authorization_url).toMatch(/checkout.paystack.com/);
});
it('fulfills order when valid charge.success webhook arrives', async () => {
var reference = 'test_ref_' + Date.now();
var payload = {
event: 'charge.success',
data: {
reference,
amount: 1000000, // NGN 10,000 in kobo
status: 'success',
metadata: { order_id: orderId },
customer: { email: 'test@example.com' },
},
};
var body = JSON.stringify(payload);
var sig = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(body)
.digest('hex');
var res = await request(app)
.post('/webhook/paystack')
.set('x-paystack-signature', sig)
.set('Content-Type', 'application/json')
.send(body);
expect(res.status).toBe(200);
var updated = await db.orders.findByPk(orderId);
expect(updated.status).toBe('paid');
expect(updated.paystack_reference).toBe(reference);
});
});
Learn More
This guide is part of the Paystack testing guide.
Key Takeaways
- ✓Integration tests use the real Paystack test API — set PAYSTACK_SECRET_KEY=sk_test_... in your test environment.
- ✓Test the initialize → webhook → fulfillment flow: call initialize, then simulate the webhook to complete the flow.
- ✓Use a test database (separate from development) and clean it between tests using transactions or truncation.
- ✓Webhook simulation in integration tests: construct the event payload and POST it to your handler with a valid HMAC signature.
- ✓Integration tests are slower than unit tests — run them in CI but not on every file save in development.
- ✓Test unhappy paths: webhook arriving before order exists, duplicate webhook for same reference, amount mismatch.
Frequently Asked Questions
- Should integration tests call the real Paystack test API or use mocks?
- Integration tests should call the real Paystack test API — that is the point of integration tests. Mocks belong in unit tests. The Paystack test API is free to use, fast, and does not move real money. If you mock the Paystack API in integration tests, you are just writing slower unit tests.
- How do I prevent integration tests from running in production?
- Set a NODE_ENV=test environment variable and verify it is set before tests run. Also verify PAYSTACK_SECRET_KEY starts with "sk_test_" — fail fast if a live key is detected. In your CI config, inject the test key explicitly so production never uses it for tests.
- Can integration tests run in a GitHub Actions CI pipeline?
- Yes. Add your PAYSTACK_TEST_KEY as a GitHub Actions secret, set it as an environment variable in the workflow YAML, and run your integration test suite. Paystack test API is accessible from CI environments — no VPN or special network setup needed.
- How do I clean up test data created during integration tests?
- Use database transactions that are rolled back after each test (most test frameworks support this). Alternatively, create test-specific records with a known test prefix in your reference or metadata, and delete records with that prefix in afterAll. Avoid sharing test data between tests — isolated state prevents flaky failures.
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