Back to Blog

SaaS Testing Strategy — What We Test, How Much, and What We Skip

Published: June 29, 2026
SaaS Testing Strategy — What We Test, How Much, and What We Skip

Every SaaS founder faces the testing dilemma one way or another. Do you aim for 100% test coverage and never ship? Or do you skip testing entirely and hope nothing breaks?

Our testing strategy is the result of shipping software 20 times. It's the result of watching features break in production, watching tests waste engineer time, and watching teams burn out chasing testing perfection that never arrives. Here is exactly what we test, what we skip, and why.

TL;DR: We use a three-speed approach: Unit tests (<2 minutes), integration tests (<10 minutes), E2E tests (30-45 minutes). We test auth, billing, multi-tenant isolation, and a handful of critical user journeys. Everything else we skip and document the decision.

SaaS testing strategy across unit, integration, and end-to-end tests on a developer workstation

The Three-Speed SaaS Testing Strategy

Our testing strategy has evolved to match how SaaS actually works — small teams, multiple deployments per week, and integration with dozens of third-party services. The strategy focuses on rapid feedback without blocking releases.

1. Unit Tests — Business Rules in Under 2 Minutes

What We Test: Business logic, data transformations, validation rules, and branching behavior.

Examples:

  • Does this discount calculation apply correctly to different subscription tiers?
  • Does user registration validation reject invalid email formats?
  • Does role-based access control correctly permit manager view but restrict payment processing?

Why We Test This: Unit tests catch the bugs that actually matter: incorrect calculations, validation errors, and edge cases. They're fast because they stub out everything they depend on — databases, caches, external services.

What We Skip: Testing framework behavior, trivial pass-through code, and low-signal snapshots that fail more often than they teach you something.

The Reality: Unit tests should complete in under two minutes on CI. If a unit test takes longer, something's wrong.

Unit Test Examples in Our Code

TypeScript
1// Example: Unit test for business rule validation
2test('should apply discount for annual subscriptions', () => {
3  const result = calculatePrice({
4    plan: 'annual',
5    users: 10,
6    discountCode: 'WELCOME20'
7  });
8  expect(result.total).toBe(800); // 10 users × $100 × 80% = $800
9});
10
11// Example: User validation unit test
12test('should reject invalid email formats', () => {
13  const result = validateUser({
14    email: 'invalid-email-format',
15    name: 'Test User'
16  });
17  expect(result.isValid).toBe(false);
18  expect(result.errors).toContain('Invalid email format');
19});

2. API Integration Tests — Boundaries That Actually Break in Production

What We Test: Database access, auth, cache, queue, file storage, and third-party API integration.

Examples:

  • Does this handler write the right rows to the database?
  • Does this auth middleware accept, reject, and refresh sessions correctly?
  • Does payment queue consumer correctly process payment.completed events?
  • Does external API adapter handle rate limits and error responses correctly?

Why We Test This: In production, 80% of our bugs happen at integration boundaries. Mocks hide these boundaries in unit tests, so integration tests are where we catch real issues.

The Three Approaches:

1. Seed-and-Reset — Fast but Requires Control

Before each test suite, we reset the database to a known fixture state. This works when we control the entire test environment.

2. Per-Test Isolated Accounts — More Realistic

Create a fresh tenant/account per test run using our API, delete after. This is slower (adds 2-5 seconds per test) but works in shared environments and mirrors real SaaS usage.

3. Per-Worker Accounts — Best Performance

In Playwright, we use testInfo.workerIndex to assign each parallel worker its own pre-provisioned test account. No cleanup needed, minimal overhead.

Integration Test Example

Two engineers reviewing integration tests as part of a SaaS testing strategy

TypeScript
1// Example: Integration test for database operations
2test('should persist user subscription changes correctly', async () => {
3  // Create test user and subscription
4  const user = await createTestUser();
5  const subscription = await upgradeSubscription(user.id, 'premium');
6
7  // Verify database state
8  const dbUser = await UserModel.findById(user.id)
9    .populate('subscription')
10    .lean();
11
12  expect(dbUser.subscription.plan).toBe('premium');
13  expect(dbUser.subscription.status).toBe('active');
14});
15
16// Example: Integration test for third-party API mocking
17test('should handle Stripe webhook correctly', async () => {
18  // Mock Stripe webhook payload
19  const mockWebhook = {
20    id: 'evt_test_123',
21    type: 'invoice.payment_succeeded',
22    data: { object: { id: 'in_test_123', status: 'paid' } }
23  };
24
25  // Call handler
26  const result = await handleStripeWebhook(mockWebhook);
27
28  // Verify internal state
29  const payment = await PaymentModel.findOne({ stripeId: 'in_test_123' });
30  expect(payment.status).toBe('completed');
31});

3. End-to-End Tests — Critical User Flows in 30-45 Minutes

What We Test: User journeys that matter to business: signup, login, core value, billing, and RBAC.

The Five Flows That Cover 80% of Critical Failures:

  1. Signup/Onboarding — New user can complete registration and access core features
  2. Authentication — Login, password reset, session management
  3. Core Value — Actual product usage that creates value for the user
  4. Billing — Upgrade/downgrade, payment processing, subscription management
  5. RBAC — Role-based access control prevents unauthorized access

Why We Test This: E2E tests catch the bugs that actually affect users: broken flows, wiring issues, and configuration mistakes.

What We Skip: UI testing for every single component. We only test the critical flows that users actually use. The rest we trust the unit and integration tests to catch.

E2E Test Example

TypeScript
1// Example: E2E test for signup flow
2 test('should complete full signup and access core features', async () => {
3   // Start signup
4   await page.goto('/signup');
5   await page.fill('[data-testid="email"]', 'newuser@example.com');
6   await page.fill('[data-testid="password"]', 'SecurePass123');
7   await page.click('[data-testid="signup-button"]');
8
9   // Wait for redirect to dashboard
10   await page.waitForURL('/dashboard');
11
12   // Verify user can access core features
13   await expect(page.locator('[data-testid="create-project"]')).toBeVisible();
14});
15
16// Example: E2E test for billing flow with feature flags
17 test('should handle plan upgrade with feature flags', async () => {
18   // Create and login user
19   const user = await createTestUser();
20   await login(user);
21
22   // Access billing page
23   await page.goto('/billing');
24
25   // Upgrade plan using feature flags to test both states
26   await page.click('[data-testid="upgrade-to-pro"]');
27   await page.waitForSelector('[data-testid="payment-form"]');
28
29   // Fill payment details and complete upgrade
30   await page.fill('[data-testid="card-number"]', '4242424242424242');
31   await page.fill('[data-testid="expiry"]', '1225');
32   await page.fill('[data-testid="cvc"]', '123');
33   await page.click('[data-testid="complete-upgrade"]');
34
35   // Verify upgrade success
36   await expect(page.locator('[data-testid="subscription-status"]')).toContainText('Pro');
37});

The Testing Pyramid for SaaS

Our testing approach follows a modified test pyramid adapted for SaaS:

Code
1                               ______________
2                              /   E2E Tests    \
3                             /  (Critical)     \
4                            /__________________\
5                           /   Integration   \
6                          /    Tests        \
7                         /____________________\
8                        /       Unit         \
9                       /_____________________\

E2E Tests (top layer): Selectively tested, critical user journeys. If they fail, we rollback. They're expensive and flaky, so we keep them to a minimum.

Integration Tests (middle layer): Boundaries that actually break in production. These are our "last line of defense" for system interactions.

Unit Tests (bottom layer): Business logic. Maximum number, fastest execution. We can write comprehensive coverage here because they're cheap and fast.

What's Most Important to Test in SaaS

Prioritizing the money paths in a SaaS testing strategy on a code monitor

Based on our 20 years of experience, these testing areas matter most:

Critical Areas We Always Test

1. Authentication Flows (Non-Negotiable)

  • Password reset flows
  • Session management
  • MFA (if we offer it)
  • Cross-tenant access bypass attempts
  • OWASP Top 10 (SQL injection, XSS, etc.)

2. Subscription and Billing (Critical)

  • Plan upgrades/downgrades
  • Payment failures and dunning
  • Refund processing
  • Trial conversions and expirations

3. Multi-Tenant Isolation (Critical)

  • One tenant cannot access another tenant's data
  • Row-level security and RLS policies
  • Resource usage isolation

4. Third-Party Integrations (Critical)

  • Stripe webhook handling
  • Email services (Resend/Postmark)
  • Auth0/Clerk integration
  • Queue systems (BullMQ)
  • Cache systems (Redis)

Areas We Test Sparingly

1. UI Components (Selective)

  • Only tested if they affect critical user flows
  • No testing for trivial UI changes

2. Performance (Subset)

  • We don't load test everything
  • Focus on critical paths: signup, core feature usage, billing

3. Security (Targeted)

  • We scan dependencies with npm audit
  • Test OWASP Top 10 only for authentication and API endpoints

Our Testing Strategy Decision Tree

Question 1: Does this test verify business-critical behavior?

  • YES → Include it (auth, billing, critical flows)
  • NO → Skip it (trivial UI changes, decorative features)

Question 2: Does this test verify a boundary that breaks in production?

  • YES → Include it (database, cache, queue, external APIs)
  • NO → Skip it (internal implementation details)

Question 3: Can this be tested without touching production?

  • YES → Use the cheaper approach (unit/integration tests)
  • NO → Only if really necessary (E2E with extreme caution)

Question 4: How much time investment vs. risk reduction?

  • High ROI → Include it
  • Low ROI → Skip it, document why

Common Testing Mistakes We Fixed

Mistake 1: Mocking Away the Exact Boundary That Can Fail

Problem: Developers mock everything, including the boundaries where real bugs happen.

Solution: We keep real integration tests for critical boundaries: database, auth, cache, queue, and external APIs. Mock third-party dependencies that we don't control.

Mistake 2: Over-Testing UI

Problem: Testing every UI change is expensive and flakey.

Solution: Test intent, not implementation. Write tests that describe "when user tries to upgrade plan, they see payment form" rather than "when user clicks button with id 'upgrade', payment form appears."

Mistake 3: Chasing 100% Test Coverage

Problem: 100% coverage often means testing getters and trivial methods that never fail.

Solution: We don't chase 100% coverage. We aim for "the auth and billing paths are covered so completely that we'd deploy on a Friday." We document what we deliberately skip.

The Production Go/No-Go Checklist

Based on our experience, before launching a new feature, we ask:

  1. Does this change affect authentication or authorization? If yes, comprehensive testing required.

  2. Does this change affect billing or subscriptions? If yes, thorough testing required.

  3. Does this involve multi-tenant data? If yes, isolation testing required.

  4. Does this call external services? If yes, integration testing required.

  5. Does this affect user workflows? If yes, E2E testing required for critical paths.

If any answer is "yes," we require corresponding testing. Everything else can wait.

Our Testing Stack

Automation Tools

  • Unit/Integration: Jest with Prisma for database testing
  • E2E: Playwright (because it's faster than Cypress and supports API testing)
  • Mocking: MSW (Mock Service Worker) for API mocking

CI/CD Integration

  • Unit tests: Run on every commit (target: <2 minutes)
  • Integration tests: Run on pull requests (target: <10 minutes)
  • E2E tests: Run on merge to main (target: <45 minutes)
  • Performance tests: Run weekly on staging

Test Data Management

  • Per-test isolated accounts: Primary approach for integration testing
  • Seed-and-reset: Secondary approach for staging tests
  • Production-like data: We never test against production data

The Bottom Line

Every SaaS faces a testing tradeoff: completeness vs. speed vs. focus. Our strategy is:

Speed > Completeness: We can ship features faster if we test intelligently.

Focus > Quantity: We test what matters, not everything.

Automation > Manual: We automate everything that doesn't require human judgment.

The result: we can ship multiple times per week, confident that the most critical paths actually work. The tests catch the bugs that actually affect users, not the ones that look good on a coverage report.

TL;DR: Our testing strategy is three-speed (unit, integration, E2E), focused on auth, billing, multi-tenant isolation, and critical user flows. We skip everything else and document the decision.

Frequently Asked Questions

We use a three-speed approach: Unit tests (under 2 minutes), API integration tests (under 10 minutes on pull requests), and end-to-end tests (30-45 minutes on staging). The unit tests cover business rules and validation logic; the integration tests verify database, auth, cache, queue, and third-party API calls work together; the E2E tests cover critical user flows on specific data.

We don't aim for 100% coverage. We aim for 'the auth and billing paths are covered so completely that you'd deploy on a Friday'— and we document what we deliberately skip. For early-stage SaaS, we typically focus on: authentication flows, subscription logic, multi-tenant isolation, critical user journeys, and third-party integrations.

Multi-tenant isolation is the biggest challenge. We solved it by using per-test isolated accounts via the API, deleting them after each test run. This ensures tests can't leak data or affect each other. For staging tests, we seed and reset the database to a known fixture state, but the API approach is more reliable.

We mock third-party APIs in CI (Stripe, email, auth services) to avoid hitting real production services. We maintain test accounts with major integration partners and run smoke tests against live services daily. This catches breaking changes early while not spending hours on unreliable integration tests.

We never skip the authentication flow testing. We test password reset, session management, MFA (if we offer it), cross-tenant access bypass attempts, and OWASP Top 10 vulnerabilities like SQL injection and XSS. Authentication failures mean users can't access the product, so we treat auth testing as non-negotiable.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio