How to Test SaaS Subscription and Billing Workflows
Billing bugs are uniquely painful. They either cost you money (customers getting features they didn't pay for) or cost you customers (users locked out of features they did pay for). Neither is acceptable, yet billing flows are frequently undertested because they involve external services, test cards, and complex state machines.
This guide covers a systematic approach to testing SaaS billing: Stripe test mode patterns, upgrade and downgrade paths, failed payment handling, and the webhook reliability that holds it all together.
Setting Up Stripe Test Mode Correctly
Before writing a single test, establish the right Stripe test environment. Everything here uses Stripe's test mode — real API calls with fake money.
Test Card Reference
Stripe provides test card numbers for specific scenarios. Know these by heart:
| Scenario | Card Number |
|---|---|
| Successful payment | 4242 4242 4242 4242 |
| Payment requires authentication | 4000 0025 0000 3155 |
| Card declined | 4000 0000 0000 0002 |
| Insufficient funds | 4000 0000 0000 9995 |
| Card expired | 4000 0000 0000 0069 |
| Fraudulent card (blocked) | 4100 0000 0000 0019 |
Keep these in a shared test utilities file so every test uses the same constants. Never hardcode them inline — when Stripe deprecates a test number, you want one place to update.
Stripe CLI for Webhook Testing
Install the Stripe CLI and use it to forward webhooks to your local server during development:
stripe listen --forward-to localhost:3000/webhooks/stripeMore importantly, use stripe trigger to fire specific events without going through the full payment flow:
stripe trigger payment_intent.payment_failed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_succeededThis decouples your webhook handler tests from your payment flow tests. You can test the webhook handler in isolation before testing the end-to-end flow.
Testing the Subscription Purchase Flow
Happy Path: New Subscription
Test: User subscribes to Pro plan successfully
1. Navigate to /pricing
2. Click "Start Pro Trial" or "Subscribe"
3. Complete Stripe Checkout with card 4242 4242 4242 4242
4. Assert: redirect to success page
5. Assert: user account shows Pro features active
6. Assert: subscription record exists in database with status=active
7. Assert: welcome email sent (check email fixture or log)Don't stop at the success page redirect. Verify the actual state change in your system.
SCA/3DS Authentication Flow
Strong Customer Authentication is required in Europe and increasingly elsewhere. You must test it:
Test: Subscription requiring 3DS authentication
1. Begin subscription flow
2. Enter card 4000 0025 0000 3155 (requires authentication)
3. Stripe presents authentication modal
4. Click "Authorize" in test modal
5. Assert: subscription created successfully
6. Assert: user account shows active subscription
Test: User fails 3DS authentication
1. Begin subscription flow
2. Enter card 4000 0025 0000 3155
3. Click "Fail" in test authentication modal
4. Assert: subscription NOT created
5. Assert: error message shown to user
6. Assert: user can retry with different cardTrial Period Handling
If you offer free trials, test the trial state machine explicitly:
Test: Trial starts correctly
- Subscribe with trial_period_days = 14
- Assert: subscription status = trialing
- Assert: no payment charged
- Assert: Pro features accessible
Test: Trial converts to paid
- Advance test clock to trial end date
- Stripe CLI: stripe trigger customer.subscription.trial_will_end
- Assert: invoice generated
- Assert: subscription status = active after successful payment
Test: Trial ends without payment method
- Subscribe with trial, no default payment method
- Advance clock to trial end
- Assert: subscription status = past_due or canceled (per your config)
- Assert: Pro features revoked
- Assert: user shown payment required promptPlan Upgrade and Downgrade Paths
These flows involve proration, immediate vs. end-of-period changes, and credit application — all of which are easy to get wrong.
Immediate Upgrade
Test: Upgrade from Free to Pro immediately
1. User on Free plan
2. Navigate to billing settings
3. Select Pro plan, confirm upgrade
4. Assert: Stripe subscription updated immediately (not at period end)
5. Assert: prorated charge calculated correctly
6. Assert: Pro features available immediately after upgrade
7. Assert: billing period reset OR prorated (per your business rules)Verify what your proration behavior is and test that specifically. Stripe's default is to prorate immediately and credit unused time — if you've changed this, test what you actually configured.
Downgrade: End-of-Period Change
Downgrades typically take effect at the end of the current billing period. This creates a window where you need to be careful:
Test: Downgrade scheduled for period end
1. User on Pro plan, mid-billing-period
2. Initiate downgrade to Free
3. Assert: subscription scheduled_phase or cancel_at_period_end set
4. Assert: Pro features STILL active until period end
5. Assert: user shown "Your plan changes on [date]" message
6. Assert: user can cancel the pending downgrade
Test: Downgrade takes effect at period end
1. Advance Stripe test clock to subscription period end
2. Assert: subscription updated to Free tier
3. Assert: Pro features no longer accessible
4. Assert: user notified of plan changeUpgrade During Trial
Test: User upgrades from trial to paid plan
1. User in trial period
2. Select paid plan
3. Add payment method and confirm
4. Assert: trial ends immediately
5. Assert: paid subscription starts
6. Assert: billing cycle begins from todayFailed Payment Handling
Failed payments are where many SaaS applications lose customers unnecessarily. Good testing here directly impacts revenue retention.
First Payment Failure
Test: Initial payment failure shows recovery flow
1. Subscribe using card 4000 0000 0000 9995 (insufficient funds)
2. Assert: error message shown (NOT generic "something went wrong")
3. Assert: error message is specific (e.g., "Your card has insufficient funds")
4. Assert: user can update card and retry
5. Assert: no partial subscription createdDunning Flow — Recurring Payment Failures
Configure your Stripe dunning settings and test that your application responds correctly to each webhook event:
Event: invoice.payment_failed (attempt 1)
- Assert: user receives "payment failed" email
- Assert: account remains active (grace period)
- Assert: UI shows payment update prompt
Event: invoice.payment_failed (attempt 2, 3 days later)
- Assert: second reminder sent
- Assert: account still active per your grace period policy
Event: invoice.payment_failed (final attempt)
- Assert: subscription transitions to past_due or canceled
- Assert: access to paid features revoked
- Assert: user receives cancellation email
- Assert: account can be reactivated by updating payment methodPayment Method Update and Retry
Test: User updates card and retries failed payment
1. Simulate invoice in past_due state
2. User navigates to billing settings
3. User adds new valid card (4242 4242 4242 4242)
4. User clicks "Retry payment" or "Update and pay"
5. Assert: Stripe retries the outstanding invoice
6. Assert: subscription status returns to active
7. Assert: paid features restored immediately
8. Assert: success confirmation shownWebhook Reliability Testing
Webhooks are the connective tissue between Stripe's state and your application's state. They must be reliable.
Idempotency
Stripe will retry webhook delivery. Your handlers must be idempotent:
Test: Duplicate webhook delivery is handled safely
1. Send payment_succeeded webhook for invoice_id X
2. Assert: subscription activated
3. Send same webhook again (simulate retry)
4. Assert: no duplicate subscription created
5. Assert: no duplicate welcome email sent
6. Assert: handler returns 200 (not 500)Use Stripe's event ID to track processed events. Store processed event IDs in your database and skip processing if you've seen the ID before.
Out-of-Order Delivery
Webhooks don't always arrive in order. Test the edge cases:
Test: subscription.updated arrives before subscription.created
- Process subscription.updated event first
- Process subscription.created event second
- Assert: final state is correct
- Assert: no errors in application logsWebhook Signature Verification
Test: Webhook with invalid signature is rejected
- POST to /webhooks/stripe with invalid Stripe-Signature header
- Assert: 400 Bad Request returned
- Assert: event NOT processed
Test: Webhook with valid signature is accepted
- POST with correct HMAC signature (use Stripe CLI or test utility)
- Assert: 200 OK returned
- Assert: event processed correctlyBilling Portal and Self-Service
If you use Stripe's Customer Portal (or build your own), test the self-service flows:
Test: User can view invoice history
- Navigate to billing settings
- Assert: list of past invoices with dates and amounts
- Assert: each invoice has download/view link
- Assert: invoice PDFs are accessible
Test: User can update payment method
- Navigate to billing settings
- Click "Update payment method"
- Enter new card details
- Assert: new card is now default
- Assert: old card removed or marked secondary
- Assert: next invoice will charge new card
Test: User can cancel subscription
- Navigate to billing settings
- Click "Cancel subscription"
- Confirm cancellation
- Assert: cancel_at_period_end set to true
- Assert: "Cancels on [date]" shown in UI
- Assert: confirmation email sentMonitoring Billing Flows in Production
Testing billing in CI is necessary but not sufficient. Billing flows involve external dependencies — Stripe's availability, webhook delivery reliability, payment processing latency. HelpMeTest's 24/7 health checks let you monitor the critical billing paths continuously: verify the pricing page loads, the checkout initiates correctly, and webhooks are being processed within expected timeframes.
With health checks running every few minutes, you catch issues like "Stripe webhook processing stopped" before your customers do — before failed payments pile up unresolved.
Common Billing Testing Mistakes
Testing only successful payments. The failure scenarios are where bugs hide and where revenue is lost. Test every failure mode.
Not testing webhook handlers in isolation. Don't rely on end-to-end tests to cover all webhook event types. Use stripe trigger to fire each event type against your handler directly.
Forgetting the subscription state machine. Stripe subscriptions move through states: trialing → active → past_due → canceled → unpaid. Test every state transition, not just the active state.
Not testing concurrent billing events. What happens if a webhook arrives while the user is in the middle of upgrading? Race conditions in billing code are real and costly.
Using real card numbers in test environments. Always use Stripe's test card numbers. Actual card numbers in test logs or databases are a PCI compliance nightmare.
Conclusion
Billing bugs are high-stakes. A systematic test suite that covers the happy path, the failure paths, plan transitions, and webhook reliability is the difference between confident billing deploys and late-night production fires.
Start with Stripe test cards, build idempotent webhook handlers, and test every state transition your subscription can go through. Your customers — and your revenue — depend on it.