Mocking Payment Providers in Unit and Integration Tests
Payment provider SDKs make real HTTP calls that cost money, have rate limits, and behave differently in different environments. Mocking payment providers in tests lets you test all payment scenarios — successful payments, declines, network errors, and edge cases — without making real API calls.
This guide covers mocking strategies for payment providers: Stripe, PayPal, and generic HTTP mocking for any provider.
Why Mock Payment Providers
Speed: Real Stripe API calls take 200-500ms. Mocked calls take microseconds. A test suite with 100 payment tests drops from 30 seconds to under 1 second.
Cost: Stripe's test mode is free, but some payment providers charge even for test transactions. Mocks eliminate this.
Coverage: You can't easily trigger all edge cases with real APIs. With mocks, you can simulate "insufficient funds", "network timeout", "invalid card", and "rate limit exceeded" in the same test suite.
Reliability: Real payment APIs are external dependencies. If Stripe has an outage, your tests shouldn't fail.
Mocking the Stripe SDK
Jest Module Mock
The simplest approach — mock the entire Stripe SDK:
// tests/__mocks__/stripe.ts
const StripeMock = jest.fn().mockImplementation(() => ({
paymentIntents: {
create: jest.fn(),
retrieve: jest.fn(),
confirm: jest.fn(),
cancel: jest.fn(),
list: jest.fn(),
},
customers: {
create: jest.fn(),
retrieve: jest.fn(),
update: jest.fn(),
del: jest.fn(),
list: jest.fn(),
},
subscriptions: {
create: jest.fn(),
retrieve: jest.fn(),
update: jest.fn(),
cancel: jest.fn(),
list: jest.fn(),
},
invoices: {
retrieve: jest.fn(),
pay: jest.fn(),
list: jest.fn(),
},
webhooks: {
constructEvent: jest.fn(),
generateTestHeaderString: jest.fn(),
},
refunds: {
create: jest.fn(),
},
checkout: {
sessions: {
create: jest.fn(),
retrieve: jest.fn(),
},
},
}));
module.exports = StripeMock;// tests/payments/stripe-service.test.ts
import Stripe from 'stripe';
import { StripePaymentService } from '~/services/stripe-payment-service';
jest.mock('stripe');
const mockStripe = new Stripe('test_key') as jest.Mocked<Stripe>;
describe('StripePaymentService', () => {
let service: StripePaymentService;
beforeEach(() => {
jest.clearAllMocks();
service = new StripePaymentService(mockStripe);
});
describe('createPaymentIntent', () => {
it('creates payment intent with correct parameters', async () => {
const mockPI = {
id: 'pi_test_123',
client_secret: 'pi_test_123_secret',
amount: 2999,
currency: 'usd',
status: 'requires_payment_method',
};
(mockStripe.paymentIntents.create as jest.Mock).mockResolvedValue(mockPI);
const result = await service.createPaymentIntent({
amount: 2999,
currency: 'usd',
metadata: { orderId: 'ord_123' },
});
expect(mockStripe.paymentIntents.create).toHaveBeenCalledWith({
amount: 2999,
currency: 'usd',
metadata: { orderId: 'ord_123' },
automatic_payment_methods: { enabled: true },
});
expect(result.clientSecret).toBe('pi_test_123_secret');
});
it('handles Stripe API errors', async () => {
const stripeError = {
type: 'StripeCardError',
code: 'card_declined',
message: 'Your card was declined.',
};
(mockStripe.paymentIntents.create as jest.Mock).mockRejectedValue(stripeError);
await expect(
service.createPaymentIntent({ amount: 2999, currency: 'usd' })
).rejects.toThrow('Your card was declined.');
});
it('handles network timeout', async () => {
(mockStripe.paymentIntents.create as jest.Mock).mockRejectedValue(
new Error('ETIMEDOUT')
);
await expect(
service.createPaymentIntent({ amount: 2999, currency: 'usd' })
).rejects.toThrow();
});
});
describe('createSubscription', () => {
it('creates subscription for a customer', async () => {
(mockStripe.subscriptions.create as jest.Mock).mockResolvedValue({
id: 'sub_test_456',
status: 'active',
current_period_end: 1735689600,
customer: 'cus_test_789',
});
const result = await service.createSubscription({
customerId: 'cus_test_789',
priceId: 'price_test_monthly',
});
expect(result.subscriptionId).toBe('sub_test_456');
expect(result.status).toBe('active');
});
it('handles payment failure on subscription create', async () => {
(mockStripe.subscriptions.create as jest.Mock).mockResolvedValue({
id: 'sub_test_456',
status: 'incomplete',
latest_invoice: {
payment_intent: {
status: 'requires_payment_method',
last_payment_error: { message: 'Card was declined.' },
},
},
});
await expect(
service.createSubscription({
customerId: 'cus_test_789',
priceId: 'price_test_monthly',
})
).rejects.toThrow('Card was declined.');
});
});
describe('processRefund', () => {
it('creates refund for correct amount', async () => {
(mockStripe.refunds.create as jest.Mock).mockResolvedValue({
id: 'ref_test_123',
amount: 1000,
status: 'succeeded',
});
const result = await service.refund({
paymentIntentId: 'pi_test_123',
amount: 1000,
reason: 'customer_request',
});
expect(mockStripe.refunds.create).toHaveBeenCalledWith({
payment_intent: 'pi_test_123',
amount: 1000,
reason: 'customer_request',
});
expect(result.refundId).toBe('ref_test_123');
expect(result.status).toBe('succeeded');
});
});
});Mocking Stripe Webhooks
// tests/webhooks/webhook-signature.test.ts
import Stripe from 'stripe';
import { verifyWebhookSignature } from '~/services/stripe-webhook';
jest.mock('stripe');
describe('Webhook signature verification', () => {
const mockStripeInstance = new Stripe('test_key') as jest.Mocked<Stripe>;
const WEBHOOK_SECRET = 'whsec_test_1234';
it('accepts valid webhook payload', () => {
const mockEvent: Stripe.Event = {
id: 'evt_test_123',
type: 'payment_intent.succeeded',
object: 'event',
api_version: '2023-10-16',
created: 1234567890,
data: {
object: {
id: 'pi_test_123',
amount: 2999,
} as Stripe.PaymentIntent,
},
livemode: false,
pending_webhooks: 0,
request: null,
};
(mockStripeInstance.webhooks.constructEvent as jest.Mock).mockReturnValue(mockEvent);
const result = verifyWebhookSignature(
mockStripeInstance,
Buffer.from(JSON.stringify(mockEvent)),
'valid-signature',
WEBHOOK_SECRET
);
expect(result).toEqual(mockEvent);
});
it('rejects invalid webhook signature', () => {
(mockStripeInstance.webhooks.constructEvent as jest.Mock).mockImplementation(() => {
throw new Error('Webhook Error: No signatures found matching the expected signature');
});
expect(() =>
verifyWebhookSignature(
mockStripeInstance,
Buffer.from('payload'),
'invalid-sig',
WEBHOOK_SECRET
)
).toThrow('No signatures found');
});
});Factory Functions for Test Data
Create factory functions to generate realistic Stripe objects:
// tests/factories/stripe-factories.ts
import type Stripe from 'stripe';
export function createPaymentIntent(
overrides: Partial<Stripe.PaymentIntent> = {}
): Stripe.PaymentIntent {
return {
id: `pi_test_${Math.random().toString(36).substr(2, 9)}`,
object: 'payment_intent',
amount: 2999,
amount_capturable: 0,
amount_details: { tip: {} } as any,
amount_received: 0,
application: null,
application_fee_amount: null,
automatic_payment_methods: null,
canceled_at: null,
cancellation_reason: null,
capture_method: 'automatic',
client_secret: 'pi_test_secret',
confirmation_method: 'automatic',
created: Math.floor(Date.now() / 1000),
currency: 'usd',
customer: null,
description: null,
invoice: null,
last_payment_error: null,
latest_charge: null,
livemode: false,
metadata: {},
next_action: null,
on_behalf_of: null,
payment_method: null,
payment_method_options: {},
payment_method_types: ['card'],
processing: null,
receipt_email: null,
review: null,
setup_future_usage: null,
shipping: null,
source: null,
statement_descriptor: null,
statement_descriptor_suffix: null,
status: 'requires_payment_method',
transfer_data: null,
transfer_group: null,
...overrides,
};
}
export function createSubscription(
overrides: Partial<Stripe.Subscription> = {}
): Stripe.Subscription {
return {
id: `sub_test_${Math.random().toString(36).substr(2, 9)}`,
object: 'subscription',
application: null,
application_fee_percent: null,
automatic_tax: { enabled: false, liability: null } as any,
billing_cycle_anchor: Math.floor(Date.now() / 1000),
billing_thresholds: null,
cancel_at: null,
cancel_at_period_end: false,
canceled_at: null,
cancellation_details: { comment: null, feedback: null, reason: null },
collection_method: 'charge_automatically',
created: Math.floor(Date.now() / 1000),
currency: 'usd',
current_period_end: Math.floor(Date.now() / 1000) + 30 * 24 * 3600,
current_period_start: Math.floor(Date.now() / 1000),
customer: 'cus_test_123',
days_until_due: null,
default_payment_method: null,
default_source: null,
default_tax_rates: [],
description: null,
discount: null,
discounts: [],
ended_at: null,
invoice_settings: { account_tax_ids: null, issuer: null } as any,
items: { object: 'list', data: [], has_more: false, url: '' },
latest_invoice: null,
livemode: false,
metadata: {},
next_pending_invoice_item_invoice: null,
on_behalf_of: null,
pause_collection: null,
payment_settings: null,
pending_invoice_item_interval: null,
pending_setup_intent: null,
pending_update: null,
plan: null,
quantity: 1,
schedule: null,
start_date: Math.floor(Date.now() / 1000),
status: 'active',
test_clock: null,
transfer_data: null,
trial_end: null,
trial_settings: null,
trial_start: null,
...overrides,
} as Stripe.Subscription;
}HTTP-Level Mocking with nock
For integration tests that exercise the full HTTP stack:
// tests/integration/stripe-api.test.ts
import nock from 'nock';
import Stripe from 'stripe';
import { PaymentService } from '~/services/payment-service';
const stripe = new Stripe('sk_test_fake_key', { apiVersion: '2023-10-16' });
const service = new PaymentService(stripe);
describe('PaymentService integration', () => {
afterEach(() => nock.cleanAll());
it('creates payment intent via Stripe API', async () => {
nock('https://api.stripe.com')
.post('/v1/payment_intents', {
amount: 2999,
currency: 'usd',
})
.reply(200, {
id: 'pi_nock_test_123',
client_secret: 'pi_nock_test_123_secret',
status: 'requires_payment_method',
amount: 2999,
currency: 'usd',
});
const result = await service.createPaymentIntent({ amount: 2999, currency: 'usd' });
expect(result.id).toBe('pi_nock_test_123');
expect(nock.isDone()).toBe(true); // All mocked requests were made
});
it('retries on network error', async () => {
nock('https://api.stripe.com')
.post('/v1/payment_intents')
.replyWithError('ECONNRESET')
.post('/v1/payment_intents')
.reply(200, { id: 'pi_retry_success', status: 'requires_payment_method' });
const result = await service.createPaymentIntentWithRetry({ amount: 1000, currency: 'usd' });
expect(result.id).toBe('pi_retry_success');
});
});Summary
Mocking payment providers in tests:
- Jest module mocks for unit tests — mock the full Stripe SDK, control every response
- Factory functions for realistic Stripe objects — avoid hand-crafting large nested objects
- Webhook mocking — mock
constructEventto return controlled Stripe.Event objects - HTTP-level mocking (nock) for integration tests — verify the actual HTTP calls your code makes
- Test all error scenarios — card declined, network timeout, invalid card, rate limits
Mock at the right level: jest mocks for unit tests (fastest), nock for integration tests (realistic), and real Stripe test mode for E2E tests (highest confidence). The combination gives you comprehensive payment testing coverage at every speed tier.