This document describes the testing strategy, patterns, and best practices for the Poppa codebase.
Poppa uses Jest as the primary testing framework with a focus on:
- API Route Tests - Testing webhook handlers and server-side logic
- Utility Tests - Testing pure functions and business logic
- Integration Tests - Testing component interactions (planned)
| Tool | Purpose |
|---|---|
| Jest | Test runner and assertion library |
| ts-jest | TypeScript support for Jest |
| node-mocks-http | Mocking Next.js API requests/responses |
| supertest | HTTP assertions (optional) |
| @testing-library/react | Component testing (for future use) |
# Run all tests
pnpm test
# Run tests in watch mode (development)
pnpm test:watch
# Run tests with coverage report
pnpm test:coverage
# Run tests in CI mode (no watch, with coverage)
pnpm test:ci
# Run specific test file
pnpm test -- src/pages/api/stripe/__tests__/webhooks.test.ts
# Run tests matching a pattern
pnpm test -- --testNamePattern="webhook"poppa_frontend/
├── jest.config.js # Jest configuration
├── jest.setup.js # Global mocks and setup
├── src/
│ ├── lib/
│ │ └── __tests__/ # Utility function tests
│ │ ├── lesson-utils.test.ts
│ │ └── supportedLanguages.test.ts
│ ├── pages/
│ │ └── api/
│ │ ├── stripe/
│ │ │ └── __tests__/
│ │ │ ├── checkout-session.test.ts
│ │ │ ├── customer-portal.test.ts
│ │ │ └── webhooks.test.ts
│ │ └── elevenlabs/
│ │ └── __tests__/
│ │ └── webhooks.test.ts
│ └── components/
│ └── __tests__/ # Component tests (future)
- Test files:
*.test.tsor*.test.tsx - Test directories:
__tests__/adjacent to the code being tested - Test descriptions: Use clear, behavior-focused descriptions
describe('ElevenLabs Webhook Handler', () => {
describe('Signature Verification', () => {
it('should return 400 if signature is missing', () => {
// test
});
});
});API route tests verify request handling, validation, and response formatting.
import { createMocks } from 'node-mocks-http';
import type { NextApiRequest, NextApiResponse } from 'next';
import handler from '../handler';
describe('/api/example', () => {
it('should handle POST requests', async () => {
const { req, res } = createMocks<NextApiRequest, NextApiResponse>({
method: 'POST',
body: { data: 'test' },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(200);
expect(res._getJSONData()).toEqual({ success: true });
});
it('should reject non-POST methods', async () => {
const { req, res } = createMocks<NextApiRequest, NextApiResponse>({
method: 'GET',
});
await handler(req, res);
expect(res._getStatusCode()).toBe(405);
});
});Pure function tests should cover edge cases and expected behavior.
import { generateThinkingMethodInstruction } from '../lesson-utils';
describe('generateThinkingMethodInstruction', () => {
it('should generate instruction for Spanish learner', () => {
const result = generateThinkingMethodInstruction('Spanish', 'English');
expect(result).toContain('Spanish');
expect(result).toContain('English');
expect(result).toContain('socratic method');
});
it('should include core teaching principles', () => {
const result = generateThinkingMethodInstruction('French', 'English');
expect(result).toContain('Never directly explain grammar rules');
expect(result).toContain('Never ask students to memorize');
});
});Follow the Arrange-Act-Assert pattern:
it('should increment user usage on call_ended event', async () => {
// Arrange
const eventPayload = {
type: 'call_ended',
data: { user_id: 'user_123' },
};
mockSupabase.rpc.mockResolvedValue({ data: [{ usage_count: 1 }] });
// Act
await handler(req, res);
// Assert
expect(mockSupabase.rpc).toHaveBeenCalledWith('increment_user_usage', {
p_user_id: 'user_123',
p_increment_by: 1,
});
expect(res._getStatusCode()).toBe(200);
});These mocks are available in all tests:
// Supabase client
jest.mock('./src/lib/supabase', () => ({
supabase: {
from: jest.fn().mockReturnThis(),
select: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
// ... other methods
},
}));
// Stripe SDK
jest.mock('stripe', () => {
return jest.fn(() => ({
checkout: { sessions: { create: jest.fn() } },
webhooks: { constructEvent: jest.fn() },
}));
});Override global mocks for specific tests:
beforeEach(() => {
jest.clearAllMocks();
// Reset to default behavior
(supabaseClient.rpc as jest.Mock).mockResolvedValue({
data: [{ usage_count: 1 }],
error: null,
});
});
it('should handle database error', async () => {
// Override for this test
(supabaseClient.rpc as jest.Mock).mockResolvedValueOnce({
data: null,
error: { message: 'Database error' },
});
await handler(req, res);
expect(res._getStatusCode()).toBe(500);
});// In jest.setup.js
process.env.STRIPE_SECRET_KEY = 'sk_test_mock';
process.env.ELEVENLABS_WEBHOOK_SECRET = 'el_whsec_mock';
// In tests, temporarily change
it('should error when secret is missing', () => {
const original = process.env.STRIPE_SECRET_KEY;
delete process.env.STRIPE_SECRET_KEY;
// ... test ...
process.env.STRIPE_SECRET_KEY = original;
});it('should log warning on usage limit exceeded', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation();
await handler(req, res);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Usage limit exceeded')
);
warnSpy.mockRestore();
});| Area | Status | Notes |
|---|---|---|
| Stripe Webhooks | ✅ Tested | Full coverage of payment events |
| ElevenLabs Webhooks | ✅ Tested | Signature verification, usage tracking |
| Utility Functions | lesson-utils, supportedLanguages | |
| Components | ❌ Not tested | Future work |
// jest.config.js (future)
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 70,
statements: 70,
},
'./src/pages/api/': {
branches: 80,
functions: 80,
lines: 80,
},
}- Critical Path - Webhook handlers (payments, usage tracking)
- Business Logic - Lesson generation, curriculum selection
- Data Integrity - Database operations, user state management
- Security - Signature verification, authentication checks
- ✅ Test behavior, not implementation
- ✅ Use descriptive test names that explain the scenario
- ✅ Clear mocks between tests (
jest.clearAllMocks()) - ✅ Test error cases and edge conditions
- ✅ Keep tests focused and independent
- ❌ Test third-party library internals
- ❌ Create complex test fixtures (keep data inline)
- ❌ Mock too much (if mocking everything, integration test may be better)
- ❌ Rely on test execution order
- ❌ Skip tests without a tracking issue
pnpm test -- --verbosepnpm test -- --testNamePattern="specific test name"node --inspect-brk node_modules/.bin/jest --runInBandThen attach VS Code debugger or open chrome://inspect.
- Component Testing - Add React Testing Library tests for UI components
- E2E Testing - Add Playwright tests for critical user flows
- Coverage Enforcement - Add coverage thresholds to CI
- Visual Regression - Screenshot testing for UI changes
- Performance Testing - Benchmark lesson generation