10 KiB
ADR-010: Testing Strategy and Standards
Date: 2025-12-12
Status: Accepted
Context
The project has a good foundation of unit and integration tests. However, there is no formal document defining the scope, purpose, and expected coverage for different types of tests. This can lead to inconsistent testing quality and gaps in coverage.
Decision
We will formalize the testing pyramid for the project, defining the role of each testing layer:
- Unit Tests (Vitest): For isolated functions, components, and repository methods with mocked dependencies. High coverage is expected.
- Integration Tests (Supertest): For API routes, testing the interaction between controllers, services, and mocked database layers. Focus on contract and middleware correctness.
- End-to-End (E2E) Tests (Vitest + Supertest): For critical user flows (e.g., login, flyer upload, checkout), running against a real test server and database to ensure the entire system works together.
Consequences
Positive: Ensures a consistent and comprehensive approach to quality assurance. Gives developers confidence when refactoring or adding new features. Clearly defines "done" for a new feature. Negative: May require investment in setting up and maintaining the E2E testing environment. Can slightly increase the time required to develop a feature if all test layers are required.
Implementation Details
Testing Framework Stack
| Tool | Version | Purpose |
|---|---|---|
| Vitest | 4.0.15 | Test runner for all test types |
| @testing-library/react | 16.3.0 | React component testing |
| @testing-library/jest-dom | 6.9.1 | DOM assertion matchers |
| supertest | 7.1.4 | HTTP assertion library for API testing |
| msw | 2.12.3 | Mock Service Worker for network mocking |
| testcontainers | 11.8.1 | Database containerization (optional) |
| c8 + nyc | 10.1.3 / 17.1.0 | Coverage reporting |
Test File Organization
src/
├── components/
│ └── *.test.tsx # Component unit tests (colocated)
├── hooks/
│ └── *.test.ts # Hook unit tests (colocated)
├── services/
│ └── *.test.ts # Service unit tests (colocated)
├── routes/
│ └── *.test.ts # Route handler unit tests (colocated)
├── utils/
│ └── *.test.ts # Utility function tests (colocated)
└── tests/
├── setup/ # Test configuration and setup files
├── utils/ # Test utilities, factories, helpers
├── assets/ # Test fixtures (images, files)
├── integration/ # Integration test files (*.test.ts)
└── e2e/ # End-to-end test files (*.e2e.test.ts)
Naming Convention: {filename}.test.ts or {filename}.test.tsx for unit/integration, {filename}.e2e.test.ts for E2E.
Configuration Files
| Config | Environment | Purpose |
|---|---|---|
vite.config.ts |
jsdom | Unit tests (React components, hooks) |
vitest.config.integration.ts |
node | Integration tests (API routes) |
vitest.config.e2e.ts |
node | E2E tests (full user flows) |
vitest.workspace.ts |
- | Orchestrates all test projects |
Test Pyramid
┌─────────────┐
│ E2E │ 5 test files
│ Tests │ Critical user flows
├─────────────┤
│ Integration │ 17 test files
│ Tests │ API contracts + middleware
┌───┴─────────────┴───┐
│ Unit Tests │ 185 test files
│ Components, Hooks, │ Isolated functions
│ Services, Utils │ Mocked dependencies
└─────────────────────┘
Unit Tests
Purpose: Test isolated functions, components, and modules with mocked dependencies.
Environment: jsdom (browser-like)
Key Patterns:
// Component testing with providers
import { renderWithProviders, screen } from '@/tests/utils/renderWithProviders';
describe('MyComponent', () => {
it('renders correctly', () => {
renderWithProviders(<MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
});
// Hook testing
import { renderHook, waitFor } from '@testing-library/react';
import { useMyHook } from './useMyHook';
describe('useMyHook', () => {
it('returns expected value', async () => {
const { result } = renderHook(() => useMyHook());
await waitFor(() => expect(result.current.data).toBeDefined());
});
});
Global Mocks (automatically applied via tests-setup-unit.ts):
- Database connections (
pg.Pool) - AI services (
@google/genai) - Authentication (
jsonwebtoken,bcrypt) - Logging (
logger.server,logger.client) - Notifications (
notificationService)
Integration Tests
Purpose: Test API routes with real service interactions and database.
Environment: node
Setup: Real Express server on port 3001, real PostgreSQL database
// API route testing pattern
import supertest from 'supertest';
import { createAndLoginUser } from '@/tests/utils/testHelpers';
describe('Auth API', () => {
let request: ReturnType<typeof supertest>;
let authToken: string;
beforeAll(async () => {
const app = (await import('../../../server')).default;
request = supertest(app);
const { token } = await createAndLoginUser(request);
authToken = token;
});
it('GET /api/auth/me returns user profile', async () => {
const response = await request
.get('/api/auth/me')
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body.user.email).toBeDefined();
});
});
Database Cleanup:
import { cleanupDb } from '@/tests/utils/cleanup';
afterAll(async () => {
await cleanupDb({ users: [testUserId] });
});
E2E Tests
Purpose: Test complete user journeys through the application.
Timeout: 120 seconds (for long-running flows)
Current E2E Tests:
auth.e2e.test.ts- Registration, login, password resetflyer-upload.e2e.test.ts- Complete flyer upload pipelineuser-journey.e2e.test.ts- Full user workflowadmin-authorization.e2e.test.ts- Admin-specific flowsadmin-dashboard.e2e.test.ts- Admin dashboard functionality
Mock Factories
The project uses comprehensive mock factories (src/tests/utils/mockFactories.ts, 1553 lines) for creating test data:
import {
createMockUser,
createMockFlyer,
createMockFlyerItem,
createMockRecipe,
resetMockIds,
} from '@/tests/utils/mockFactories';
beforeEach(() => {
resetMockIds(); // Ensure deterministic IDs
});
it('creates flyer with items', () => {
const flyer = createMockFlyer({ store_name: 'TestMart' });
const items = [createMockFlyerItem({ flyer_id: flyer.flyer_id })];
// ...
});
Factory Coverage: 90+ factory functions for all domain entities including users, flyers, recipes, shopping lists, budgets, achievements, etc.
Test Utilities
| Utility | Purpose |
|---|---|
renderWithProviders() |
Wrap components with AppProviders + Router |
createAndLoginUser() |
Create user and return auth token |
cleanupDb() |
Database cleanup respecting FK constraints |
createTestApp() |
Create Express app for route testing |
poll() |
Polling utility for async operations |
Coverage Configuration
Coverage Provider: v8 (built-in Vitest)
Report Directories:
.coverage/unit/- Unit test coverage.coverage/integration/- Integration test coverage.coverage/e2e/- E2E test coverage
Excluded from Coverage:
src/index.tsx,src/main.tsx(entry points)src/tests/**(test files themselves)src/**/*.d.ts(type declarations)src/components/icons/**(icon components)src/db/seed*.ts(database seeding scripts)
npm Scripts
# Run all tests
npm run test
# Run by level
npm run test:unit # Unit tests only (jsdom)
npm run test:integration # Integration tests only (node)
# With coverage
npm run test:coverage # Unit + Integration with reports
# Clean coverage directories
npm run clean
Test Timeouts
| Test Type | Timeout | Rationale |
|---|---|---|
| Unit | 5 seconds | Fast, isolated tests |
| Integration | 60 seconds | AI service calls, DB operations |
| E2E | 120 seconds | Full user flow with multiple API calls |
Best Practices
When to Write Each Test Type
-
Unit Tests (required):
- Pure functions and utilities
- React components (rendering, user interactions)
- Custom hooks
- Service methods with mocked dependencies
- Repository methods
-
Integration Tests (required for API changes):
- New API endpoints
- Authentication/authorization flows
- Middleware behavior
- Database query correctness
-
E2E Tests (for critical paths):
- User registration and login
- Core business flows (flyer upload, shopping lists)
- Admin operations
Test Isolation Guidelines
- Reset mock IDs: Call
resetMockIds()inbeforeEach() - Unique test data: Use timestamps or UUIDs for emails/usernames
- Clean up after tests: Use
cleanupDb()inafterAll() - Don't share state: Each test should be independent
Mocking Guidelines
- Unit tests: Mock external dependencies (DB, APIs, services)
- Integration tests: Mock only external APIs (AI services)
- E2E tests: Minimal mocking, use real services where possible
Key Files
vite.config.ts- Unit test configurationvitest.config.integration.ts- Integration test configurationvitest.config.e2e.ts- E2E test configurationvitest.workspace.ts- Workspace orchestrationsrc/tests/setup/tests-setup-unit.ts- Global mocks (488 lines)src/tests/setup/integration-global-setup.ts- Server + DB setupsrc/tests/utils/mockFactories.ts- Mock factories (1553 lines)src/tests/utils/testHelpers.ts- Test utilities
Future Enhancements
- Browser E2E Tests: Consider adding Playwright for actual browser testing
- Visual Regression: Screenshot comparison for UI components
- Performance Testing: Add benchmarks for critical paths
- Mutation Testing: Verify test quality with mutation testing tools
- Coverage Thresholds: Define minimum coverage requirements per module