Mocking Payment APIs in Unit Tests: stripe-mock, nock, and VCR

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-mock

stripe-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 nock

Basic 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 vcrpy

Basic 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: 1

Scrubbing 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 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.

Read more