stripe-test-utils
v0.1.0
Published
Test utilities for Node.js applications integrating Stripe — type-safe event factories, signed webhooks, scenario builders, and custom matchers
Maintainers
Readme
stripe-test-utils
Test utilities for Node.js applications integrating Stripe.
Generates realistic, type-safe Stripe event payloads — signed and ready to use — without hitting the network or copying JSON by hand.
Installation
# npm
npm install --save-dev stripe-test-utils stripe
# pnpm
pnpm add -D stripe-test-utils stripesupertest is optional and only needed for Express support:
npm install --save-dev supertest
# or
pnpm add -D supertestCompatibility
npm SDK version — the peer dependency supports stripe ^14 through ^22. Install whichever version your application uses.
Stripe REST API version — distinct from the SDK version. The generated events include a default api_version field matching the latest Stripe API version. If your webhook handler checks that field, override it:
stripeEvent('payment_intent.succeeded', {
api_version: '2024-06-20',
})stripeEvent(type, overrides?)
Generates a complete Stripe.Event with sensible defaults. Each call produces a fresh, independent set of IDs.
import { stripeEvent } from 'stripe-test-utils'
// complete event with all required fields
const event = stripeEvent('payment_intent.succeeded')
// partial override — deep merged, non-overridden fields preserved
const event = stripeEvent('payment_intent.succeeded', {
data: { object: { amount: 9900, currency: 'eur' } },
})
// TypeScript infers the exact event type
// event: Stripe.PaymentIntentSucceededEvent
event.data.object.status // 'succeeded'Supported event types
| Group | Events |
|---|---|
| PaymentIntent | payment_intent.created payment_intent.succeeded payment_intent.payment_failed payment_intent.canceled |
| Subscription | customer.subscription.created customer.subscription.updated customer.subscription.deleted customer.subscription.trial_will_end |
| Invoice | invoice.created invoice.finalized invoice.payment_succeeded invoice.payment_failed |
| Customer | customer.created customer.updated customer.deleted payment_method.attached |
| Checkout | checkout.session.completed checkout.session.expired |
| Charge | charge.succeeded charge.failed charge.refunded charge.dispute.created |
stripeEvent.signed(type, options)
Generates a signed payload + stripe-signature header, ready for end-to-end webhook handler testing including signature verification.
import { stripeEvent } from 'stripe-test-utils'
const { payload, header } = stripeEvent.signed('payment_intent.succeeded', {
secret: process.env.STRIPE_WEBHOOK_SECRET!,
overrides: { data: { object: { amount: 9900 } } },
})With supertest (Express)
const res = await request(app)
.post('/webhooks/stripe')
.set('stripe-signature', header)
.set('content-type', 'application/json')
.send(payload)
expect(res.status).toBe(200)With Fastify inject
const res = await app.inject({
method: 'POST',
url: '/webhooks/stripe',
headers: { 'stripe-signature': header, 'content-type': 'application/json' },
payload,
})
expect(res.statusCode).toBe(200)Options
| Option | Type | Description |
|---|---|---|
| secret | string | Webhook signing secret (whsec_…) |
| overrides | DeepPartial<Event> | Optional event overrides |
| timestamp | number | Unix timestamp for the signature (default: Date.now()) |
stripeScenario(name)
Returns an ordered array of Stripe.Event objects representing a real Stripe lifecycle. All IDs are consistent across events within the same scenario — same customer.id, subscription.id, etc.
import { stripeScenario } from 'stripe-test-utils'
const events = stripeScenario('subscription.created_and_active')
for (const event of events) {
await handleWebhook(event)
}Available scenarios
subscription.created_and_active — 6 events
customer.created
payment_method.attached
customer.subscription.created
invoice.created
invoice.payment_succeeded
customer.subscription.updated ← status: activepayment.failed_then_succeeded — 4 events
payment_intent.created ← first attempt (pi_A)
payment_intent.payment_failed ← fails
payment_intent.created ← retry (pi_B, same customer)
payment_intent.succeededcheckout.completed_with_subscription — 3 events
customer.created
customer.subscription.created
checkout.session.completed ← references same customer & subscriptionsetupStripeMatchers()
Custom Jest/Vitest matchers for readable assertions. Call once in your setup file.
// jest.setup.ts (or vitest.setup.ts)
import { setupStripeMatchers } from 'stripe-test-utils/matchers'
setupStripeMatchers()// in your tests
import { stripeEvent } from 'stripe-test-utils'
const event = stripeEvent('payment_intent.succeeded', {
data: { object: { amount: 9900, currency: 'eur', metadata: { orderId: '42' } } },
})
expect(event).toBeStripeEvent('payment_intent.succeeded')
expect(event).toHaveStripeAmount(9900)
expect(event).toHaveStripeCurrency('eur')
expect(event).toHaveStripeCustomer('cus_abc123')
expect(event).toHaveStripeMetadata({ orderId: '42' })Matcher reference
| Matcher | Description |
|---|---|
| toBeStripeEvent(type) | Checks event.type and event.object === 'event' |
| toHaveStripeAmount(n) | Checks event.data.object.amount |
| toHaveStripeCurrency(c) | Checks event.data.object.currency (case-insensitive) |
| toHaveStripeCustomer(id) | Checks event.data.object.customer — works with string ID or expanded object |
| toHaveStripeMetadata(obj) | Checks that event.data.object.metadata contains the expected key/value subset |
testWebhookHandler(app, options)
Tests a webhook handler end-to-end without running a server. Automatically detects Express vs Fastify via duck typing.
import { testWebhookHandler, stripeEvent } from 'stripe-test-utils'
// with a pre-built event
const result = await testWebhookHandler(app, {
route: '/webhooks/stripe',
event: stripeEvent('payment_intent.succeeded'),
secret: process.env.STRIPE_WEBHOOK_SECRET,
})
expect(result.status).toBe(200)
// with event type shorthand (builds the event internally)
const result = await testWebhookHandler(app, {
route: '/webhooks/stripe',
event: 'invoice.payment_succeeded',
secret: 'whsec_test',
})When secret is provided, the request is signed and your handler's stripe.webhooks.constructEvent call will succeed. When omitted, the raw JSON is sent without a signature.
Options
| Option | Type | Description |
|---|---|---|
| route | string | The webhook route path |
| event | Stripe.Event \| SupportedEventType | Event object or type string |
| secret | string | Optional signing secret |
Result
| Field | Type | Description |
|---|---|---|
| status | number | HTTP status code |
| body | unknown | Parsed JSON response body |
| headers | Record<string, string> | Response headers |
ID consistency
Within a single stripeEvent() call or stripeScenario(), all cross-references use the same ID. For example:
payment_intent.data.object.customerandinvoice.data.object.customershare the samecus_…ID in a scenariocustomer.subscription.data.object.idmatchesinvoice.data.object.subscriptionin the same scenario
IDs follow Stripe's prefix conventions: cus_, pi_, sub_, in_, ch_, pm_, cs_, evt_.
License
MIT
