Bonaventure OgetoBy Bonaventure Ogeto|

Mocking the Paystack API in Jest

Mock the Paystack API in Jest by using jest.mock() on your Paystack wrapper module, or by intercepting fetch calls with jest.spyOn(global, "fetch"). Return mock resolved values for success cases and rejected promises for error cases. This lets you test your handler logic for every scenario without network calls.

Mocking a Paystack Wrapper Module

// lib/paystack.js — your wrapper
const fetch = require('node-fetch');

async function initializeTransaction({ email, amount, reference }) {
  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY },
    body: JSON.stringify({ email, amount, reference }),
  });
  return res.json();
}

async function verifyTransaction(reference) {
  var res = await fetch('https://api.paystack.co/transaction/verify/' + reference, {
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY },
  });
  return res.json();
}

module.exports = { initializeTransaction, verifyTransaction };

// --- payment.test.js ---
jest.mock('./lib/paystack', () => ({
  initializeTransaction: jest.fn(),
  verifyTransaction: jest.fn(),
}));

const paystack = require('./lib/paystack');
const { createCheckout } = require('./payment-service');

describe('createCheckout', () => {
  beforeEach(() => jest.clearAllMocks());

  it('returns authorization_url on success', async () => {
    paystack.initializeTransaction.mockResolvedValue({
      status: true,
      data: {
        authorization_url: 'https://checkout.paystack.com/abc',
        reference: 'ref_001',
      },
    });

    var result = await createCheckout({ email: 'user@test.com', amount: 100000 });
    expect(result.authorization_url).toBe('https://checkout.paystack.com/abc');
  });

  it('throws on Paystack API failure', async () => {
    paystack.initializeTransaction.mockRejectedValue(new Error('Network error'));
    await expect(createCheckout({ email: 'user@test.com', amount: 100000 })).rejects.toThrow('Network error');
  });
});

Mocking fetch Directly

// If your code calls fetch directly without a wrapper
global.fetch = jest.fn();

describe('verifyPayment', () => {
  it('returns success data when transaction is paid', async () => {
    global.fetch.mockResolvedValueOnce({
      json: () => Promise.resolve({
        status: true,
        data: { status: 'success', amount: 100000, reference: 'ref_001' },
      }),
    });

    var result = await verifyPayment('ref_001');
    expect(result.data.status).toBe('success');
  });

  it('handles 5xx server error from Paystack', async () => {
    global.fetch.mockResolvedValueOnce({
      ok: false,
      status: 500,
      json: () => Promise.resolve({ status: false, message: 'Server error' }),
    });

    await expect(verifyPayment('ref_bad')).rejects.toThrow();
  });

  afterEach(() => jest.resetAllMocks());
});

Learn More

Key Takeaways

  • Use jest.mock() to replace your Paystack wrapper module — all imports of the module in the code under test get the mock.
  • For code that calls fetch directly, use jest.spyOn(global, "fetch").mockResolvedValue() to intercept HTTP calls.
  • Mock different responses for different test cases: success, decline, network error, 5xx server error.
  • Reset mocks between tests with jest.clearAllMocks() or afterEach(() => jest.restoreAllMocks()).
  • Test the charge.success, charge.failed, and error scenarios in separate test cases — do not combine them.
  • Avoid nock or other HTTP interceptors when a simple jest.mock on your wrapper module is sufficient.

Frequently Asked Questions

Should I use nock or msw to mock Paystack API calls?
For unit tests of a Paystack wrapper module, jest.mock() is the simplest approach — no extra libraries needed. For integration tests where you want to intercept actual HTTP calls from multiple modules, msw (Mock Service Worker) is a clean option. Nock works for Node.js but requires careful cleanup between tests.
How do I test that my code retries on a 429 rate limit response?
Mock the first call to return a 429 response and the second to return success. Then call your retry-aware function and assert it called the mocked fetch twice. Example: global.fetch.mockResolvedValueOnce({ status: 429, json: () => ({ message: "Rate limit" }) }).mockResolvedValueOnce({ status: 200, json: () => ({ status: true, data: {} }) });
Do I need to set PAYSTACK_SECRET_KEY in Jest tests?
When you mock the Paystack wrapper module with jest.mock(), the actual API call never happens, so no real key is needed. If you test code that reads process.env.PAYSTACK_SECRET_KEY directly (e.g., to construct headers), set it in your jest.config.js or in the test file: process.env.PAYSTACK_SECRET_KEY = "sk_test_fake";
What is the difference between mockResolvedValue and mockResolvedValueOnce?
mockResolvedValue sets the return value for all future calls to that mock. mockResolvedValueOnce sets it for only the next call — subsequent calls use the default return value or the next queued value. Use mockResolvedValueOnce when testing sequences (e.g., first call returns error, second returns success for retry testing).

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