Skip to main content

Published Updated

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 annotations 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 annotation 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