Skip to main content

Testing in Boilerplates: Why Most Skip It (And Why That's a Problem)

·StarterPick Team
testingvitestplaywrightboilerplate2026

TL;DR

Most boilerplates skip testing because founders don't buy boilerplates to write tests — they buy them to ship fast. The Epic Stack is the clear outlier: it ships with Vitest, Playwright, and MSW fully configured. For production SaaS, a basic testing setup (critical path E2E + utility unit tests) is worth 2-3 days of setup and saves weeks of debugging.

The Testing Gap in Boilerplates

Checking major boilerplates for included test infrastructure:

BoilerplateUnit TestsE2E TestsTest ConfigMocking
Epic Stack✅ Vitest✅ Playwright✅ Full✅ MSW
T3 Stack
ShipFast
SupastarterMinimalMinimal
Makerkit✅ Vitest✅ Playwright
Open SaaS

Why Boilerplates Skip Tests

Market incentives: Boilerplate buyers evaluate by features visible in the README. "Stripe billing" sells boilerplates. "100% test coverage" does not.

Boilerplate code is hard to test: Auth flows, Stripe webhooks, database operations — the core boilerplate features require mocking that's non-trivial to set up.

Tests go stale: A boilerplate test for auth that worked last year may fail when the auth library updates. Maintaining tests for a product you sell once is a business cost.

The honest reality: most commercial boilerplates skip tests to reduce maintenance burden and improve sales conversion. This means you inherit untested code.


The Epic Stack: Testing Done Right

Kent C. Dodds' Epic Stack is the gold standard for testing in boilerplates. It ships with a complete testing setup:

# Epic Stack ships with all of this configured:
npx create-epic-app@latest my-saas

# Run unit tests
npm run test

# Run E2E tests
npm run test:e2e

# Coverage report
npm run test:coverage

Vitest for Unit/Integration Tests

// app/utils/pricing.test.ts
import { describe, it, expect } from 'vitest';
import { calculateProration, getFeatures, isFeatureEnabled } from './pricing';

describe('calculateProration', () => {
  it('returns 0 when downgrading mid-cycle with no remaining days', () => {
    const result = calculateProration({
      currentPlan: 'pro',
      newPlan: 'free',
      daysRemainingInCycle: 0,
    });
    expect(result).toBe(0);
  });

  it('calculates correct credit for upgrade mid-cycle', () => {
    const result = calculateProration({
      currentPlan: 'free',
      newPlan: 'pro',
      daysRemainingInCycle: 15,
      cycleLength: 30,
    });
    expect(result).toBe(14.99);  // Half of $29.99 pro price
  });
});

describe('isFeatureEnabled', () => {
  it('blocks pro features for free users', () => {
    expect(isFeatureEnabled('advanced-analytics', 'free')).toBe(false);
  });

  it('allows pro features for pro users', () => {
    expect(isFeatureEnabled('advanced-analytics', 'pro')).toBe(true);
  });
});

Playwright for E2E Tests

// tests/e2e/auth.test.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('user can sign up with email and password', async ({ page }) => {
    await page.goto('/sign-up');
    await page.fill('[name=email]', 'test@example.com');
    await page.fill('[name=password]', 'securepassword123');
    await page.click('[type=submit]');

    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByText('Welcome')).toBeVisible();
  });

  test('invalid credentials show error', async ({ page }) => {
    await page.goto('/sign-in');
    await page.fill('[name=email]', 'wrong@example.com');
    await page.fill('[name=password]', 'wrongpassword');
    await page.click('[type=submit]');

    await expect(page.getByText('Invalid credentials')).toBeVisible();
    await expect(page).toHaveURL('/sign-in');
  });
});

test.describe('Billing', () => {
  test('user can upgrade to pro plan', async ({ page, context }) => {
    // Log in as free user
    await loginAsTestUser(page, 'free');
    await page.goto('/pricing');
    await page.click('button:has-text("Upgrade to Pro")');

    // Stripe test card
    await page.frameLocator('[name=cardNumber]').fill('4242424242424242');
    await page.frameLocator('[name=expiry]').fill('12/28');
    await page.frameLocator('[name=cvc]').fill('123');
    await page.click('[type=submit]');

    await expect(page).toHaveURL('/dashboard?upgrade=success');
    await expect(page.getByText('Pro')).toBeVisible();
  });
});

MSW for Mocking External APIs

// tests/mocks/handlers.ts — Mock Stripe, email, and external APIs
import { http, HttpResponse } from 'msw';

export const handlers = [
  // Mock Stripe webhook
  http.post('/api/webhooks/stripe', async ({ request }) => {
    const body = await request.json();
    // Handle the event
    return HttpResponse.json({ received: true });
  }),

  // Mock external price check
  http.get('https://api.stripe.com/v1/prices/*', () => {
    return HttpResponse.json({
      id: 'price_test',
      unit_amount: 2999,
      currency: 'usd',
    });
  }),
];

Minimal Testing Setup for Any Boilerplate

If your boilerplate doesn't ship with tests, here's a minimal but meaningful setup:

npm install -D vitest @vitest/ui @playwright/test msw happy-dom
npx playwright install
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'happy-dom',
    globals: true,
    setupFiles: ['./tests/setup.ts'],
  },
});

The 20% That Prevents 80% of Bugs

Focus testing effort on:

  1. Business logic utilities — pricing, permissions, calculations
  2. Auth flow — sign-up, sign-in, protected routes
  3. Stripe webhook handling — subscription created, payment failed
  4. Critical user paths — the actions that make you money
// Test the things that break silently and cost money
describe('Stripe webhook: subscription.created', () => {
  it('activates user subscription on successful payment', async () => {
    const event = createStripeEvent('customer.subscription.created', {
      customer: 'cus_test',
      status: 'active',
      metadata: { userId: 'user_123', plan: 'pro' },
    });

    await handleWebhook(event);

    const user = await prisma.user.findUnique({ where: { id: 'user_123' } });
    expect(user?.plan).toBe('pro');
    expect(user?.subscriptionStatus).toBe('active');
  });
});

CI Integration

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm ci
      - run: npm run test -- --reporter=verbose
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
      env:
        DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

Find boilerplates with testing setup on StarterPick.

Check out this boilerplate

View Epic Stack on StarterPick →

Comments