Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 082 / 100

Building a test strategy for a SaaS app, explained simply

What to test and where.

Expona Admin8 min read

After 82 days of learning Playwright, you've built solid test skills—but knowing how to write tests and knowing which tests to write are two different problems. Building a test strategy for a SaaS app, explained simply, comes down to making deliberate choices about what to test, where to test it, and when to revisit those choices. A good strategy saves your team from test bloat, keeps your confidence high, and prevents the "we have 10,000 tests but still ship bugs" scenario.

Critical journeys — Sign-up, login, billing and core workflow

Start by identifying the paths through your SaaS that, if broken, would immediately alert customers or hurt revenue. For most products, this includes sign-up, login, the core feature workflow, and billing. These aren't optional—they're the spine of your product.

Sign-up and login are often overlooked because they feel straightforward. But a broken sign-up means zero new customers. A broken login means paying customers can't use your product. Test the happy path: valid email, strong password, confirmation email flow (or magic link). Then test the edge cases that will bite you: duplicate email, weak password, session timeout after login.

Here's a minimal sign-up test that covers the critical journey:

typescript

test('user can complete sign-up flow', async ({ page }) => {
  await page.goto('/signup');
  
  const email = `user-${Date.now()}@example.com`;
  const password = 'SecurePass123!';
  
  await page.fill('input[name="email"]', email);
  await page.fill('input[name="password"]', password);
  await page.fill('input[name="confirm-password"]', password);
  
  await page.click('button:has-text("Create Account")');
  
  // Verify redirect to onboarding or dashboard
  await page.waitForURL('/onboarding', { timeout: 5000 });
  
  // Verify user is logged in by checking for profile menu
  await expect(page.locator('[data-testid="profile-menu"]')).toBeVisible();
});

Core workflow is where your users spend their time. For a CRM, this might be creating a contact and logging a call. For a project management tool, it's creating a project and assigning a task. For a trading platform, it's placing an order. Test this once end-to-end in a clean state, then test variations: missing required fields, invalid data, concurrent actions.

Billing deserves its own suite. You need to verify:

  • Upgrade from free to paid plan
  • Downgrade from paid to free
  • Cancel subscription
  • Invoice generation and download
  • Payment method update

Billing is often handled by a third-party service (Stripe, Recurly, Paddle), so your tests should mock or use their test mode. Never run real transactions in your automated suite.

typescript

test('user can upgrade to pro plan', async ({ page }) => {
  // Login first
  await loginAs(page, 'test@example.com');
  
  // Navigate to billing
  await page.goto('/settings/billing');
  await expect(page.locator('text=Current Plan: Free')).toBeVisible();
  
  // Click upgrade button
  await page.click('button:has-text("Upgrade to Pro")');
  
  // Modal or redirect to Stripe checkout
  await page.waitForURL(/stripe\.com|checkout/, { timeout: 8000 });
  
  // In test mode, we'd verify the redirect happened
  // In production, we'd verify the webhook or poll the API
  const plan = await page.locator('[data-testid="plan-name"]').textContent();
  expect(plan).toContain('Pro');
});

Per-role coverage — Admin and member permissions

Most SaaS apps have multiple user roles, and permissions bugs are silent killers. An admin can see things they shouldn't, a member can access a resource they shouldn't, or someone without a role can trigger a workflow they shouldn't. These are security issues and user trust issues.

Create at least one test per role that verifies the boundaries. You don't need to test every permission pair—that matrix explodes fast—but you do need to test the high-risk boundaries.

For an admin role: verify they can create users, delete users, view usage reports, and change billing settings. Then verify that non-admins cannot.

For a member or guest role: verify they can access shared resources but cannot delete, modify permissions, or access admin pages.

typescript

test('non-admin cannot access admin panel', async ({ page }) => {
  // Login as a regular member
  await loginAs(page, 'member@example.com');
  
  // Try to navigate directly to admin panel
  await page.goto('/admin/users');
  
  // Should be redirected or see a forbidden page
  await page.waitForURL(/\/dashboard|\/forbidden/, { timeout: 5000 });
  
  // Verify we're not on the admin page
  await expect(page.locator('[data-testid="admin-panel"]')).not.toBeVisible();
});

test('admin can create and revoke member access', async ({ page }) => {
  // Login as admin
  await loginAs(page, 'admin@example.com');
  
  // Navigate to member management
  await page.goto('/admin/members');
  
  // Create a new member
  await page.click('button:has-text("Add Member")');
  await page.fill('input[name="email"]', 'newmember@example.com');
  await page.selectOption('select[name="role"]', 'viewer');
  await page.click('button:has-text("Invite")');
  
  // Verify member was added
  await expect(page.locator('text=newmember@example.com')).toBeVisible();
  
  // Revoke access
  await page.click('button[aria-label="Remove newmember@example.com"]');
  await page.click('button:has-text("Confirm")');
  
  // Verify member is removed
  await expect(page.locator('text=newmember@example.com')).not.toBeVisible();
});

The key insight: test the boundary, not every permission. If you have ten roles, you don't need 100 tests. Test that role A can do X and role B cannot. That pattern propagates.

Smoke in production — A few read-only checks after deploy

Smoke tests are lightweight, read-only checks that run after your deploy finishes. They're not replacing your full test suite—they're a safety net. They verify that the app is up, the main pages load, and the API responds.

Smoke tests should be:

  • Fast (run in under 30 seconds total)
  • Read-only (don't create data, don't call billing APIs)
  • Resilient to timing issues (use generous timeouts)
  • Run in production or production-like environment

A smoke test suite might look like:

typescript

// smoke.spec.ts
// Run after every deploy to production

test('homepage loads and renders', async ({ page }) => {
  const startTime = Date.now();
  
  await page.goto('https://app.example.com/', { waitUntil: 'networkidle' });
  
  const loadTime = Date.now() - startTime;
  console.log(`Homepage loaded in ${loadTime}ms`);
  
  // Very basic checks
  await expect(page.locator('text=Welcome')).toBeVisible();
  await expect(page.locator('[data-testid="logo"]')).toBeVisible();
});

test('login page is accessible', async ({ page }) => {
  await page.goto('https://app.example.com/login');
  
  // Just verify page structure, don't actually log in
  await expect(page.locator('input[name="email"]')).toBeVisible();
  await expect(page.locator('button:has-text("Sign In")')).toBeVisible();
});

test('api health check', async ({ request }) => {
  const response = await request.get('https://api.example.com/health');
  
  expect(response.status()).toBe(200);
  
  const body = await response.json();
  expect(body.status).toBe('ok');
});

test('authenticated user can view dashboard', async ({ page }) => {
  // Use a pre-created test account or service account
  await loginAs(page, process.env.TEST_ACCOUNT_EMAIL);
  
  await page.goto('https://app.example.com/dashboard');
  
  // Verify major sections load
  await expect(page.locator('[data-testid="sidebar"]')).toBeVisible();
  await expect(page.locator('[data-testid="main-content"]')).toBeVisible();
});

Run smoke tests in your deployment pipeline. If any smoke test fails, pause the rollout. The goal isn't comprehensive coverage—it's catching deploy-time disasters.

Review quarterly — Remove low-value tests, add missing ones

Test suites grow. Without regular pruning, you end up with redundant tests, brittle tests that fail on UI changes, and tests that don't match what your product actually does anymore.

Every quarter, set aside an hour to review:

  • Which tests fail most often? Flaky tests erode confidence. Fix them or remove them.
  • Which features changed? Your old tests may not reflect the current product. Update or delete them.
  • Which bugs made it to production? Add a test for each one. That's where your coverage gaps are.
  • Which tests are slowest? Slow tests kill CI/CD. Consider parallelizing, mocking, or removing them if they're redundant.
  • Which tests are most critical to your business? Ensure those are rock solid. Spend your effort there.

Keep a simple spreadsheet or issue tracker with columns for test name, last run time, failure rate, and business impact. Sort by impact and failure rate. Tests with high failure and low impact are candidates for deletion.

As your product changes, your test strategy changes too. A quarterly review keeps your suite aligned with reality.

Common mistakes

  • Testing too much UI, not enough behavior. A pixel-perfect screenshot test is fragile. Test that the button works, not that the button is exactly 42px from the edge.
  • Ignoring performance in tests. A test that takes 3 minutes per run will bankrupt your CI. Keep individual tests under 10 seconds.
  • Not testing error paths. What happens when a payment fails? When an API returns 500? When the user loses internet? These break in production; test them.
  • Testing third-party services directly. Don't make real calls to Stripe, SendGrid, or AWS in your tests. Mock them or use sandbox/test mode.
  • Assuming coverage means quality. 90% code coverage with bad tests is worse than 40% with good tests. Coverage is a guide, not a goal.
  • Not rotating test data or credentials. Hardcoded test IDs and users become stale. Use factories, randomization, or dynamic setup.

Key takeaways

  • Start with critical journeys: sign-up, login, core workflows, and billing. These are non-negotiable.
  • Test permissions per role at the boundary; don't test every role-permission pair.
  • Use smoke tests in production as a safety net after deploy, not a replacement for full coverage.
  • Flaky tests and low-value tests should be removed, not tolerated. Regular review keeps your suite healthy.
  • A good test strategy balances confidence with speed. Fewer, better tests beat a massive suite.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles