Skip to content

Testing Strategy

Purpose: This document defines our testing philosophy, conventions, and tooling. Read this once to understand the "how" and "why" of testing at show.fm.

Testing Philosophy

The Testing Trophy

We follow a modified testing trophy approach, emphasizing integration tests as the highest-value investment:

        ___
       /   \      E2E Tests (few)
      /     \     - Critical user journeys only
     /_______\    - Expensive to run & maintain
     |       |
     |       |    Integration Tests (many) ← SWEET SPOT
     |_______|    - Business flows across services
        | |       - Mocked external services, real internal logic
        | |
        | |       Unit Tests (many)
        | |       - Pure functions, utilities
        | |       - Fast, isolated
        ===
     Static       TypeScript catches type errors at compile time

Core Principles

  1. Test behavior, not implementation: Assert on outcomes (what data was passed, what response came back), not just that a function was called.

  2. Mock at boundaries: Mock external services (Supabase, Stripe, R2, Resend, Google APIs) but test real internal logic.

  3. Business logic coverage matters more than line coverage: 100% line coverage with no flow testing is worse than 60% coverage with comprehensive flow tests.

  4. Tests are documentation: A test file should read like a specification of what the code does.

When to Write What

SituationTest TypeLocationExample
Pure function, no side effectsUnit__tests__/ adjacent to sourceparseMagicTags(), escapeSearchQuery()
Single service integrationUnit + Mocks__tests__/ adjacent to sourceStripe checkout, R2 upload
Cross-service business flowIntegration__tests__/ in entry point dirBooking → Calendar → Email
Database queries/mutationsUnit + Mock Supabase__tests__/ adjacent to sourceFTS search, CRUD operations
Shared middleware behaviorIntegrationsrc/api/middleware/__tests__/Rate limiting, auth enforcement contracts
Critical user journeyE2E (Playwright)tests/ root directoryComplete booking flow
Email/notification contentUnittemplates/__tests__/Email HTML generation

Test Stack

yaml
unit_and_integration:
  runner: [email protected]
  config: vitest.config.ts
  environment: jsdom (default), node (workers)
  coverage: v8 provider
  pattern: src/**/__tests__/*.test.ts

e2e:
  runner: playwright
  config: playwright.config.ts
  pattern: tests/**/*.spec.ts

worker_tests:
  runner: vitest
  config: workers/[worker]/vitest.config.ts
  pattern: workers/**/__tests__/*.test.ts

Running Tests

bash
# All unit/integration tests
pnpm test

# Watch mode during development
pnpm test:watch

# Specific module
pnpm test src/lib/automation

# With coverage report
pnpm test:coverage

# E2E tests
pnpm test:e2e

# Worker tests
cd workers/automation-executor && pnpm test

File Structure Conventions

src/
├── api/routes/
│   └── bookings/
│       ├── index.ts                    # Route implementation
│       └── __tests__/
│           ├── index.test.ts           # Unit tests for routes
│           └── automation-integration.test.ts  # Integration tests
├── api/middleware/
│   ├── rate-limit.ts
│   └── __tests__/
│       └── rate-limit.test.ts          # Middleware contract tests
├── api/routes/guests/
│   ├── index.ts
│   └── __tests__/
│       └── index.test.ts               # Route-level limiter wiring
├── lib/
│   └── automation/
│       ├── events.ts                   # Module implementation
│       ├── scheduler.ts
│       └── __tests__/
│           ├── events.test.ts          # Unit tests
│           └── scheduler.test.ts
tests/
└── booking-flow.spec.ts                # E2E tests (Playwright)

Naming Conventions

TypePatternExample
Unit test{module}.test.tsevents.test.ts
Integration test{feature}-integration.test.tsautomation-integration.test.ts
E2E test{flow}.spec.tsbooking-flow.spec.ts

Mocking Patterns

Supabase (Database)

typescript
import { createMockSupabaseClient } from '$test';

// Use the chainable mock factory
const mockSupabase = createMockSupabaseClient({
	automation_rules: {
		select: [{ id: 'rule-123', is_enabled: true }]
	}
});

vi.mock('@supabase/supabase-js', () => ({
	createClient: () => mockSupabase
}));

Stripe (Payments)

typescript
vi.mock('stripe', () => ({
	default: class MockStripe {
		checkout = {
			sessions: { create: vi.fn().mockResolvedValue({ id: 'cs_test', url: '...' }) }
		};
		webhooks = {
			constructEvent: vi.fn().mockReturnValue(mockEvent)
		};
	}
}));

R2 (Storage)

typescript
import { createMockR2Bucket } from '$test';

const mockBucket = createMockR2Bucket();
// Supports: put, get, delete, list, head with in-memory storage

Google APIs (Calendar)

typescript
vi.mock('$lib/google-calendar/auth', () => ({
	refreshAccessToken: vi.fn().mockResolvedValue({ access_token: 'ya29.mock' }),
	exchangeCodeForTokens: vi.fn().mockResolvedValue({ access_token: '...', refresh_token: '...' })
}));

Resend (Email)

typescript
vi.mock('resend', () => ({
	Resend: class MockResend {
		emails = { send: vi.fn().mockResolvedValue({ id: 'email-123' }) };
	}
}));

Behavior Assertions (CRITICAL)

Bad - Only verifies the method was called:

typescript
expect(mockSupabaseUpdate).toHaveBeenCalled(); // Weak

Good - Verifies WHAT was passed:

typescript
expect(mockSupabaseUpdate).toHaveBeenCalledWith({
	subscription_tier: 'professional',
	stripe_customer_id: 'cus_123',
	subscription_status: 'active'
}); // Strong - catches bugs where wrong data is written

Assertion Checklist

For each test, verify the actual data:

OperationWhat to Assert
DB writeExact payload passed to insert/update
API callRequest body, headers, URL
Queue messageMessage structure and contents
Email sendRecipient, subject, body content
ResponseStatus code AND response body

Security Testing (Required for API/DB code)

Every API route test file should include:

typescript
describe('Security', () => {
	it('should reject unauthenticated requests', async () => {
		const res = await route.request(reqWithoutAuth);
		expect(res.status).toBe(401);
	});

	it('should enforce RLS - user cannot access other podcasts', async () => {
		// Test cross-tenant access attempt
	});

	it('should validate and sanitize inputs', async () => {
		// Test with XSS vectors, SQL injection patterns
	});
});

Rate-Limit Coverage (Required for Public Endpoints)

For endpoints using rateLimit(...), add both test layers:

  1. Middleware contract test (src/api/middleware/__tests__/rate-limit.test.ts)
    • asserts 429 behavior, Retry-After, X-RateLimit-*, identifier modes, and fail-open behavior
  2. Route wiring test (adjacent route __tests__/index.test.ts)
    • asserts the route is actually protected and short-circuits before handler side effects
bash
pnpm test src/api/middleware/__tests__/rate-limit.test.ts
pnpm test src/api/routes/guests/__tests__/index.test.ts

Accessibility Testing (Required for UI Components)

typescript
import { axe, toHaveNoViolations } from 'jest-axe';

expect.extend(toHaveNoViolations);

it('should have no accessibility violations', async () => {
	const { container } = render(Component);
	expect(await axe(container)).toHaveNoViolations();
});

Coverage Thresholds

Coverage thresholds are enforced in vitest.config.ts. After adding tests:

  1. Run pnpm test:coverage
  2. If coverage improved, update thresholds (always round DOWN)
  3. Never decrease thresholds without discussion
typescript
// vitest.config.ts
coverage: {
  thresholds: {
    statements: 35,
    branches: 30,
    functions: 25,
    lines: 35
  }
}

What We Don't Test

  • shadcn UI components (src/lib/components/ui/) - Third-party, well-tested
  • Type definitions (src/lib/types/) - No runtime behavior
  • Generated code - Database types, etc.
  • Test utilities (src/test/) - Testing the tests

Complex Flow Testing (Scout → Generate Workflow)

For horizontal flows that span multiple services, we use a two-stage pipeline. The scout stage assesses complexity to determine the appropriate workflow.

Stage 1: Scout the Flow

bash
/scout-test-flow [flow-name]

Scout traces the flow and scores its complexity based on 6 factors:

FactorSimple (0 pts)Complex (1 pt)
File count≤3 files>3 files
Hop count≤2 hops>2 hops
Worker boundarySame runtimeCrosses to worker
External services1 service2+ services
Time-based schedulingNo delaysHas scheduled jobs
Multiple triggersSingle triggerMultiple trigger types

Total score determines output:

  • Score 0-1 (Simple): Scout outputs a direct command—no spec file created
  • Score 2+ (Complex): Scout creates a spec file at docs/testing/flows/[flow-name]-spec.md

Stage 2: Generate Tests

The generate command has two modes depending on scout output:

Discovery Mode (Simple Flows)

When scout provides a direct command:

bash
/generate-tests [flow-name] path/to/file1.ts path/to/file2.ts

Generate explores the provided files and discovers the test scenarios.

Spec-Driven Mode (Complex Flows)

When scout created a spec file:

bash
/generate-tests --spec=docs/testing/flows/[flow-name]-spec.md

Generate reads only the files listed in the spec and uses the defined scenarios as the test plan. After generating tests, the spec file is updated with the generated test file locations and scenario coverage.

Why Two Stages?

Complex flows can involve 10+ files across multiple services. The spec file acts as a compressed "blueprint" that preserves context across conversations and tracks which scenarios have tests.

Fresh Conversation

Always run /generate-tests in a fresh conversation to avoid context buildup from the scout phase.

Stage 3: Sync Domain Map

After tests are generated, update the Domain Map to track coverage:

bash
/sync-domain-map --auto

This command:

  • Detects recently created/modified test files
  • Updates flow statuses (🔴 → 🟢)
  • Adds new files to the Test File Index
  • Recalculates Quick Stats

You can also target specific files or domains:

bash
/sync-domain-map src/lib/automation/__tests__/executor.test.ts
/sync-domain-map --domain automation

Stage 4: Update Internal Docs

Finally, update any related feature documentation:

bash
/write-internal-docs testing

Adding New Tests Checklist

  • [ ] Test file in correct location (__tests__/ adjacent to source)
  • [ ] Named correctly (*.test.ts for unit, *-integration.test.ts for integration)
  • [ ] External services mocked (Supabase, Stripe, R2, Resend, Google)
  • [ ] Behavior assertions verify actual data, not just method calls
  • [ ] Security tests included for API routes
  • [ ] Coverage thresholds updated if improved
  • [ ] package.json script added if new module: "test:modulename": "vitest run src/lib/modulename"

Quick Reference

CommandPurpose
pnpm testRun all tests
pnpm test:watchWatch mode
pnpm test:coverageCoverage report
pnpm test:e2ePlaywright E2E
pnpm test src/lib/XTest specific module

Internal documentation - Not for public distribution