Testing in Boilerplates: Why Most Skip It (And Why That's a Problem)
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:
| Boilerplate | Unit Tests | E2E Tests | Test Config | Mocking |
|---|---|---|---|---|
| Epic Stack | ✅ Vitest | ✅ Playwright | ✅ Full | ✅ MSW |
| T3 Stack | ❌ | ❌ | ❌ | ❌ |
| ShipFast | ❌ | ❌ | ❌ | ❌ |
| Supastarter | Minimal | ❌ | Minimal | ❌ |
| 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:
- Business logic utilities — pricing, permissions, calculations
- Auth flow — sign-up, sign-in, protected routes
- Stripe webhook handling — subscription created, payment failed
- 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 →