Testland
Browse all skills & agents

stripe-test-cards-and-webhooks

Wraps Stripe API testing patterns: test-mode initialization, the canonical test cards (4242 success; 4000 0000 0000 0002 declined; 4000 0027 6000 3184 3DS challenge per 3ds-test-flow-reference), the Stripe CLI webhook flow (`stripe listen --forward-to`), the Stripe CLI fixture commands (`stripe trigger payment_intent.succeeded`), and the webhook signature verification (Stripe-Signature header + HMAC-SHA256). Use when testing Stripe-integrated code. Composes payment-flow-states-reference + 3ds-test-flow-reference + pci-dss-scope-reference.

stripe-test-cards-and-webhooks

Overview

Stripe's test-mode is feature-complete: every API call works against test data, no real money moves. Per docs.stripe.com/testing, test keys (sk_test_* / pk_test_*) accept canonical test cards that deterministically produce success / decline / 3DS challenge.

When to use

  • Tests for code that integrates Stripe.
  • Verifying webhook handling.
  • 3DS challenge flow tests per 3ds-test-flow-reference.

Authoring

Install

npm install stripe
pip install stripe

Test keys come from the Stripe Dashboard (Developers → API keys → toggle to "View test data").

Initialize

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_TEST_KEY!);

Canonical test cards

Per docs.stripe.com/testing:

CardBehaviour
4242 4242 4242 4242Success (Visa)
5555 5555 5555 4444Success (Mastercard)
4000 0000 0000 0002Decline (generic_decline)
4000 0000 0000 9995Decline (insufficient_funds)
4000 0000 0000 9987Decline (lost_card)
4000 0000 0000 0069Expired card
4000 0027 6000 31843DS authentication required (challenge)
4000 0000 0000 30553DS supported but frictionless
4100 0000 0000 0019Fraud-prevention block

Test PAN any future expiry; any 3-digit CVC.

PaymentIntent end-to-end

test('successful payment via 4242', async () => {
  const intent = await stripe.paymentIntents.create({
    amount: 1000,
    currency: 'usd',
    payment_method: 'pm_card_visa',
    confirm: true,
    return_url: 'https://example.com/return',
  });
  expect(intent.status).toBe('succeeded');
});

test('declined payment', async () => {
  await expect(
    stripe.paymentIntents.create({
      amount: 1000,
      currency: 'usd',
      payment_method: 'pm_card_chargeDeclined',
      confirm: true,
      return_url: 'https://example.com/return',
    })
  ).rejects.toThrow(/card was declined/);
});

Webhook handler test

import { buffer } from 'micro';
import handler from './stripe-webhook';

test('webhook handler validates signature', async () => {
  const payload = JSON.stringify({ type: 'payment_intent.succeeded', data: {...} });
  const signature = stripe.webhooks.generateTestHeaderString({
    payload,
    secret: process.env.STRIPE_WEBHOOK_SECRET!,
  });

  const req = { headers: { 'stripe-signature': signature }, body: Buffer.from(payload) };
  const res = await handler(req as any);
  expect(res.statusCode).toBe(200);
});

Per docs.stripe.com/webhooks/signatures: the Stripe CLI's generateTestHeaderString produces a valid HMAC-SHA256 signature for testing.

Stripe CLI local forwarding

Per docs.stripe.com/stripe-cli:

stripe listen --forward-to http://localhost:3000/webhooks/stripe
# Forwards real Stripe webhook events to your local endpoint
stripe trigger payment_intent.succeeded
# Sends a synthetic event to test your handler

The CLI also exposes a signing secret (different from your prod webhook secret) for local testing.

Idempotency

test('idempotency key prevents duplicate charges', async () => {
  const key = 'order-12345';
  const r1 = await stripe.paymentIntents.create({...}, { idempotencyKey: key });
  const r2 = await stripe.paymentIntents.create({...}, { idempotencyKey: key });
  expect(r1.id).toBe(r2.id);  // Same PaymentIntent
});

Running

npm test

For webhook-integration tests with stripe listen running:

stripe listen --forward-to http://localhost:3000/webhooks/stripe &
npm test

CI integration

jobs:
  stripe-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
      - run: npm ci && npm test
        env:
          STRIPE_TEST_KEY: ${{ secrets.STRIPE_TEST_KEY }}
          STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}

Anti-patterns

Anti-patternWhy it failsFix
Mocking Stripe SDK directlyLoses signature verification, error mappingTest against real test-mode API
Hardcoded test cards in many testsUpdate breakage when Stripe changesPer 3ds-test-flow-reference, use named constants
Skip webhook signature verificationWebhook replay attackAlways verify
Tests without idempotency keyRetried tests duplicate-createAlways set idempotency
Mix prod + test keysReal money riskStrict per-env key separation
Test webhook handler without raw bodybody-parser strips bytes; signature mismatchUse raw body middleware
One-off webhook event testsPer-event-type integration coverage missedTest the full event matrix relevant to your flow
Long-running stripe listen in CIHangs buildsUse stripe listen --print-secret then background

Limitations

  • Test mode is real API surface. Rate limits apply; long CI runs can hit them.
  • Dispute timelines compress in test mode. Use Stripe CLI trigger to fast-forward dispute states.
  • Webhook delivery has ~30s SLA. Tests waiting for webhooks need timeouts.
  • Doesn't validate platform-side bookkeeping. Stripe reports are separate.

References