Mocking Payment APIs in Unit Tests: stripe-mock, nock, and VCR
Payment API calls in unit tests create slow, flaky tests with real rate limits and network dependencies. The solution is mocking at the HTTP layer — stripe-mock (a local Stripe API server), nock (Node.js HTTP interceptor), or VCR cassettes (recorded responses replayed in tests). Each approach has trade-offs. This guide covers all three with practical examples.
Key Takeaways
stripe-mock gives you a real Stripe API without network calls. stripe-mock is an open-source local server that implements the Stripe API spec. Point your Stripe SDK at it and your tests run locally with no network, no rate limits, and deterministic responses.
nock intercepts at the Node.js http module level. In Node.js, nock intercepts outgoing HTTP requests before they leave the process. It's ideal for mocking any HTTP-based payment API including Stripe, PayPal, and Braintree.
VCR cassettes record real API responses once, replay forever. With vcrpy in Python, your tests make real API calls the first time and record the responses. Subsequent runs replay from disk. You get realistic responses without ongoing API calls.
Mock the interface, not the implementation. Don't mock Stripe's internal SDK classes. Mock at the HTTP boundary so your serialization, URL construction, and error parsing are actually tested.
Cassettes can contain sensitive data. VCR cassettes capture raw HTTP responses including API keys in URLs and customer data in bodies. Review cassette files before committing and add them to .gitignore or use cassette scrubbing.
Why Mock Payment APIs in Unit Tests
Calling the real Stripe API (even test mode) in unit tests has costs:
- Speed — each API call takes 100-500ms. A test suite with 50 payment-related tests adds 5-25 seconds
- Flakiness — network errors, rate limits, and Stripe API changes can break tests intermittently
- State pollution — each test creates real Stripe objects (customers, charges, subscriptions) that accumulate in your test dashboard
- CI complexity — CI needs Stripe credentials and network access
For unit tests, mocking the HTTP layer eliminates all of these. Integration tests (which should use real Stripe test mode) are a separate concern.
stripe-mock
stripe-mock is an official Stripe-maintained local server that implements the Stripe API spec. It returns realistic mock responses based on the Stripe OpenAPI spec.
Install and run
# macOS
brew install stripe/stripe-cli/stripe-mock
<span class="hljs-comment"># Docker
docker run --<span class="hljs-built_in">rm -p 12111:12111 stripe/stripe-mock
<span class="hljs-comment"># Direct download
curl -L https://github.com/stripe/stripe-mock/releases/latest/download/stripe-mock_linux_amd64.tar.gz <span class="hljs-pipe">| tar xz
./stripe-mockstripe-mock runs on port 12111 by default.
Configure Stripe SDK to use stripe-mock
# Python
import stripe
stripe.api_key = "sk_test_any_value" # stripe-mock ignores the key
stripe.api_base = "http://localhost:12111"
def test_create_customer():
customer = stripe.Customer.create(
email="test@example.com",
name="Test User",
)
# stripe-mock returns a realistic Customer object
assert customer.id.startswith("cus_")
assert customer.email == "test@example.com"
def test_create_payment_intent():
intent = stripe.PaymentIntent.create(
amount=5000,
currency="usd",
)
assert intent.id.startswith("pi_")
assert intent.amount == 5000
assert intent.status == "requires_payment_method"Node.js with stripe-mock
const Stripe = require('stripe');
const stripe = Stripe('sk_test_any_value', {
host: 'localhost',
port: 12111,
protocol: 'http',
});
test('creates a customer', async () => {
const customer = await stripe.customers.create({
email: 'test@example.com',
});
expect(customer.id).toMatch(/^cus_/);
expect(customer.email).toBe('test@example.com');
});Using stripe-mock in pytest with a fixture
# conftest.py
import pytest
import stripe
import subprocess
import time
import socket
@pytest.fixture(scope="session")
def stripe_mock():
"""Start stripe-mock for the test session."""
port = 12111
proc = subprocess.Popen(
["stripe-mock", "-port", str(port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
# Wait for stripe-mock to be ready
for _ in range(20):
try:
with socket.create_connection(("localhost", port), timeout=1):
break
except OSError:
time.sleep(0.2)
stripe.api_key = "sk_test_mock"
stripe.api_base = f"http://localhost:{port}"
yield
proc.terminate()
proc.wait()nock (Node.js)
nock intercepts HTTP requests at the http/https module level. Any HTTP call your code makes — whether through axios, the Stripe SDK, or fetch — is intercepted before it leaves the process.
Install
npm install --save-dev nockBasic nock usage with Stripe
// test/payment-service.test.js
const nock = require('nock');
const { processPayment } = require('../src/payment-service');
afterEach(() => {
nock.cleanAll();
});
test('processes successful payment', async () => {
// Intercept Stripe PaymentIntent create call
nock('https://api.stripe.com')
.post('/v1/payment_intents')
.reply(200, {
id: 'pi_test_123',
object: 'payment_intent',
amount: 5000,
currency: 'usd',
status: 'succeeded',
client_secret: 'pi_test_123_secret_abc',
});
const result = await processPayment({
amount: 5000,
currency: 'usd',
paymentMethod: 'pm_card_visa',
});
expect(result.success).toBe(true);
expect(result.paymentIntentId).toBe('pi_test_123');
});
test('handles card decline', async () => {
nock('https://api.stripe.com')
.post('/v1/payment_intents')
.reply(402, {
error: {
type: 'card_error',
code: 'card_declined',
message: 'Your card was declined.',
decline_code: 'generic_decline',
},
});
await expect(
processPayment({ amount: 5000, currency: 'usd', paymentMethod: 'pm_bad_card' })
).rejects.toThrow('card_declined');
});Intercepting webhook delivery from your app
nock can also intercept outgoing webhook calls if your application sends webhooks to other services:
test('notifies order service after payment', async () => {
// Mock Stripe API
nock('https://api.stripe.com')
.post('/v1/payment_intents')
.reply(200, { id: 'pi_123', status: 'succeeded' });
// Mock outgoing webhook to order service
const orderWebhook = nock('https://orders.internal')
.post('/webhooks/payment-succeeded')
.reply(200, { received: true });
await processPayment({ amount: 5000, currency: 'usd' });
// Verify the order service was notified
expect(orderWebhook.isDone()).toBe(true);
});VCR (Python)
vcrpy records real HTTP interactions to "cassette" files and replays them in subsequent test runs. Tests make real API calls once, then run offline forever.
Install
pip install vcrpyBasic VCR usage
# test_stripe_vcr.py
import vcr
import stripe
stripe.api_key = "sk_test_your_test_key"
@vcr.use_cassette("cassettes/create_customer.yaml")
def test_create_customer_with_vcr():
customer = stripe.Customer.create(
email="test@example.com",
name="Test User",
)
assert customer.id is not None
assert customer.email == "test@example.com"First run: makes a real Stripe API call and saves to cassettes/create_customer.yaml. Subsequent runs: replays from the cassette — no network, same response.
Cassette file example
# cassettes/create_customer.yaml
interactions:
- request:
body: email=test%40example.com&name=Test+User
headers:
Authorization:
- Bearer sk_test_REDACTED
method: POST
uri: https://api.stripe.com/v1/customers
response:
body:
string: '{"id":"cus_test123","object":"customer","email":"test@example.com","name":"Test
User",...}'
status:
code: 200
message: OK
version: 1Scrubbing sensitive data
Cassettes capture API keys and customer data. Scrub them before committing:
import vcr
# Configure VCR to redact sensitive headers and body fields
my_vcr = vcr.VCR(
filter_headers=[("Authorization", "Bearer REDACTED")],
filter_post_data_parameters=[
("card[number]", "REDACTED"),
("card[cvc]", "REDACTED"),
],
cassette_library_dir="tests/cassettes",
)
@my_vcr.use_cassette("create_payment_intent.yaml")
def test_create_payment_intent_vcr():
intent = stripe.PaymentIntent.create(
amount=5000,
currency="usd",
)
assert intent.status == "requires_payment_method"pytest-recording (recommended)
pytest-recording is a pytest plugin built on vcrpy that integrates cleanly with pytest fixtures:
pip install pytest-recording# pytest.ini
[pytest]
addopts = --vcr-record=none # Use cassettes, fail if missing
# During cassette creation: --vcr-record=all# test_payment.py
import pytest
@pytest.mark.vcr
def test_create_charge():
import stripe
charge = stripe.Charge.create(
amount=2000,
currency="usd",
source="tok_visa",
description="Test charge",
)
assert charge.status == "succeeded"Run with --vcr-record=all once to create cassettes, then switch to --vcr-record=none for CI.
Choosing the Right Tool
| Tool | Best for | Language | Pros | Cons |
|---|---|---|---|---|
| stripe-mock | Stripe-specific unit tests | Any | No real API calls, fast, realistic | Stripe only, needs local server |
| nock | Node.js HTTP mocking | Node.js | Intercepts any HTTP call | Node.js only |
| vcrpy | Quick setup, existing tests | Python | Records real responses, minimal code change | Cassettes can contain sensitive data |
| pytest-recording | pytest-based Python tests | Python | Clean integration | Same cassette caveats as vcrpy |
Use stripe-mock when you have many Stripe-specific tests and want to run a test server alongside your test suite.
Use nock when you're in Node.js and need to mock multiple HTTP services, not just Stripe.
Use vcrpy when you want to quickly add mocking to existing tests without changing them, or when you need realistic responses from a real API.
What Not to Mock
Don't mock payment APIs in integration tests. Integration tests should use Stripe test mode (real HTTP, fake money) to verify your full stack. Reserve mocking for:
- Unit tests of payment service classes
- Tests of error handling logic
- Tests of webhook handler logic (construct signed payloads instead)
- High-volume test suites where real API calls would hit rate limits
The test pyramid applies: many fast mocked unit tests, fewer slower integration tests against Stripe test mode.
Summary
stripe-mock, nock, and VCR are three tools for the same job: eliminating real HTTP calls from unit tests. stripe-mock is the most realistic for Stripe-specific tests. nock is the most flexible for Node.js. VCR is the fastest to add to existing Python tests. All three eliminate network flakiness and make payment-related unit tests run in milliseconds.