How to Test SaaS Subscription and Billing Workflows

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

More 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_succeeded

This 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 card

Trial 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 prompt

Plan 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 change

Upgrade 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 today

Failed 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 created

Dunning 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 method

Payment 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 shown

Webhook 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 logs

Webhook 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 correctly

Billing 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 sent

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

Read more