Skip to main content

How to Test Free Trial to Paid Conversion

Short answer

Trial conversion is core SaaS revenue. E2E must cover trial expiry without payment method, mid-trial upgrade with proration, and feature lock after grace—using seeded trial_end, Stripe test clocks, and entitlement probes on APIs—not banner copy alone.

Part of Testing Guides by business flow.

Who this is for

SaaS with time-limited trials, freemium upsell, or card-required trials who need reliable conversion tests without waiting two weeks in CI.

Why testing trial conversion matters

  • ARR — silent lock without upgrade path churns; accidental charge without consent creates disputes.
  • Product — trial users hit limits; upgrade modal must not block export/delete.
  • Support — "trial expired but still charged" from clock skew between app and Stripe.

Complexity map

ScenarioEdge caseWhy tests breakApproach
No card on fileTrial endsData loss fear untestedProbe grace + export access
Card on fileTrial endsAuto-charge failsDecline card at conversion
Mid-trial upgradeProrationWrong invoiceProbe invoice + entitlements
Feature lockDay after expiryAPI still 200Probe 403 on premium API
Grace period3-day graceLock too earlySeed trial_end + grace config
Trial extensionAdmin extendsStale UIProbe trial_end updated
Downgrade during trialAllowed?Wrong stateProbe plan remains trial
Team trialSeat limitsInvite blockedLink seat licensing
Coupon on convertFirst invoiceDiscount missingProbe invoice discount
Cancel during trialNo chargeSub canceledProbe status trialing→canceled
TimezoneUTC vs local expiryOff-by-oneProbe ISO trial_end
Re-trial abuseSame emailSecond trialProbe policy rejection

Trial without payment method

await request.post('/api/test/seed-trial-user', {
data: { runId, trialDaysRemaining: 0, paymentMethod: null },
});

const ent = await request.get(`/api/test/probes/entitlements/${runId}`).then(r => r.json());
expect(ent.trialState).toBe('expired');
expect(ent.canAccessPremium).toBe(false);

const api = await request.get('/api/v1/reports/advanced', {
headers: { Authorization: `Bearer ${ent.token}` },
});
expect(api.status()).toBe(403);

Trial → paid with test clock

const { clockId, userId } = await request.post('/api/test/seed-trial-user', {
data: { runId, trialDays: 14, attachClock: true, addPaymentMethod: true },
}).then(r => r.json());

await request.post('/api/test/advance-clock', { data: { clockId, to: 'trial_end' } });

await expect.poll(async () => {
const sub = await request.get(`/api/test/probes/subscriptions/${runId}`).then(r => r.json());
return sub.status;
}, { timeout: 45_000 }).toBe('active');

const ent = await request.get(`/api/test/probes/entitlements/${userId}`).then(r => r.json());
expect(ent.canAccessPremium).toBe(true);

Mid-trial upgrade

await request.post('/api/test/seed-trial-user', {
data: { runId, plan: 'pro_trial', trialDaysRemaining: 7 },
});
await page.goto('/settings/upgrade');
await page.getByRole('button', { name: 'Upgrade to Enterprise' }).click();
await completeStripeCheckout(page, runId);

const invoice = await request.get(`/api/test/probes/invoices/latest/${runId}`).then(r => r.json());
expect(invoice.lines.some(l => l.description?.includes('Unused time'))).toBe(true);

See subscriptions billing for proration detail.

UX vs API Assert

Upgrade prompts and modals are optional UI Assert—premium API 403 and subscription probe are mandatory.

Anti-patterns

Anti-patternWhy it failsBetter approach
Wait 14 daysNever runsClock or past trial_end seed
UI "Trial expired" onlyAPI openProbe entitlements
Skip no-card pathCommon prod segmentDedicated scenario
No grace testAngry usersProbe grace flags

Example scenario

Situation: Trial expires; user has no payment method on file.

Expected outcome: Premium API locked after grace; data export still allowed per policy; upgrade flow collects PM.

Why UI-only automation breaks: Banner shows expired but API returns 200 for premium routes.

  1. Arrange: Seed user with trial_end in past, no PM, grace_days=3 if applicable.
  2. Act: Call premium API; attempt upgrade checkout.
  3. Assert: Probe 403 then 200 after successful upgrade and subscription active.

TestChimp workflow: Track trial_state × plan_tier in TrueCoverage after pricing experiments.

Same Arrange/Act/Assert pattern as expired-coupon checkout.

Connect scenarios to your QA workflow

Capture business rules in markdown test plans and enforce them with seed routes and probe Assert. Link SmartTests with // @Scenario: for requirement traceability. Use /testchimp test on PRs; /testchimp explore on SmartTest paths for non-functional gaps (ExploreChimp).

External references

Frequently asked questions

How do I test trial expiry without waiting two weeks?

Seed user with trial_end in past or use Stripe test clock to advance billing period, then assert feature lock and upgrade flow via probes.

Should I test trial conversion with and without a card on file?

Yes—both are common prod paths. No-card tests grace and upgrade PM collection; with-card tests auto-conversion and decline at trial end.

How do mid-trial upgrades differ from post-trial conversion?

Mid-trial may prorate immediately; probe invoice lines and entitlements upgrade before trial_end. Post-trial creates new active subscription from expired state.

What about data retention after trial expiry?

Probe export endpoints and deletion policy flags—product-specific but often regulated; do not only assert marketing copy on pricing page.

Can I use the same trial user in parallel workers?

No—seed per runId with unique user and Stripe customer metadata e2e_run.

Feature lock vs read-only mode?

Probe each premium endpoint your product defines; some allow read-only analytics—encode in entitlement matrix tests.

Which trial exit paths happen in production?

Compare trial_state × plan_tier in TrueCoverage after pricing experiments. Run /testchimp evolve for missing conversion paths with // @Scenario: links.

Apply these patterns in your repo

Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.

Start free on TestChimp · Book a demo