Testing
This guide covers the testing strategy, test structure, running tests, and best practices for Sessiq.
Testing Strategy
Test Pyramid
The testing strategy follows the test pyramid:
/\
/E2E\ ← Few end-to-end tests
/─────\
/Integration\ ← More integration tests
/─────────────\
/ Unit Tests \ ← Many unit tests
/─────────────────\
Distribution:
- Unit Tests: 70% - Fast, isolated, test individual functions
- Integration Tests: 25% - Test module interactions
- E2E Tests: 5% - Test complete workflows
Test Types
Unit Tests
Purpose: Test individual functions and classes in isolation.
Characteristics:
- Fast execution
- No external dependencies
- Mock external services
- Test edge cases
Example:
describe('PriceResolverService', () => {
it('should calculate tier price correctly', () => {
const price = calculateTierPrice(rule, partySize);
expect(price).toBe(expectedPrice);
});
});
Integration Tests
Purpose: Test interactions between modules.
Characteristics:
- Use test database
- Real module interactions
- Test database operations
- Test API endpoints
Example:
describe('Booking Creation Flow', () => {
it('should create booking with payment', async () => {
const booking = await createBooking(dto);
expect(booking.status).toBe('CONFIRMED');
});
});
E2E Tests
Purpose: Test complete workflows from start to finish.
Characteristics:
- Full stack testing
- Real database
- External services mocked
- User journey simulation
Example:
describe('Booking E2E Flow', () => {
it('should complete booking from search to confirmation', async () => {
// Complete user journey
});
});
Test Structure
Directory Organization
apps/api/
├── src/
│ └── modules/
│ └── bookings/
│ ├── bookings.service.ts
│ └── __tests__/
│ ├── bookings.service.spec.ts # Unit tests
│ └── bookings.integration.spec.ts # Integration tests
tests/
├── unit/ # Unit tests
├── integration/ # Integration tests
└── e2e/ # End-to-end tests
e2e/
└── stack.e2e.ts # Full stack E2E tests
Test File Naming
- Unit Tests:
*.spec.ts - Integration Tests:
*.integration.spec.tsor intests/integration/ - E2E Tests:
*.e2e.tsor ine2e/
Test Configuration
Test Framework Configuration
The project uses a hybrid approach for testing:
- Vitest for unit tests (fast, excellent watch mode)
- Jest for integration and e2e tests (better NestJS decorator support)
Jest Configuration (apps/api/jest.config.js):
- Handles both integration (
*.int.spec.ts) and e2e (*.e2e.spec.ts) tests - Configured with
ts-jestandemitDecoratorMetadata: truefor NestJS support - Uses global setup for database migrations and test data
Vitest Configuration (apps/api/vitest/vitest.unit.config.ts):
- Unit tests only (
tests/unit/**/*.spec.ts) - Fast execution with excellent watch mode
- No decorator issues since unit tests don't use complex NestJS decorators
Test Scripts
# Run all API tests
pnpm run test:api
# Run unit tests only (Vitest)
pnpm run test:api:unit
# Run integration tests only (Jest)
pnpm run test:api:int
# Run E2E tests (Jest)
pnpm run test:api:e2e
# Run tests in watch mode
pnpm -F api exec vitest watch
# Run specific test file
pnpm -F api exec vitest run path/to/test.spec.ts
Unit Testing
Unit tests use Vitest for fast execution and excellent watch mode.
Setup
Test Utilities:
import { describe, it, expect, beforeEach } from 'vitest';
describe('MyService', () => {
beforeEach(() => {
// Setup for each test
});
it('should do something', () => {
// Test implementation
});
});
Mocking
Service Mocking:
import { vi } from 'vitest';
const mockService = {
getData: vi.fn().mockResolvedValue(mockData),
};
// Use in tests
expect(mockService.getData).toHaveBeenCalled();
Database Mocking:
import { mockRlsContext } from './test-helpers/mock-rls-context';
beforeEach(() => {
mockRlsContext.setTenantId('test-tenant');
});
Best Practices
- Isolate Dependencies: Mock external services
- Test Edge Cases: Boundary conditions, errors
- Fast Execution: Keep tests under 100ms
- Clear Names: Descriptive test names
- One Assertion: Focus each test on one behavior
Integration Testing
Integration tests use Jest for reliable NestJS decorator support and reflect-metadata handling.
Database Setup
Test Database:
# Use separate test database
DATABASE_URL=postgres://user:pass@localhost:5433/booking_test
Migration Setup:
beforeAll(async () => {
await migrateTestDatabase();
});
afterAll(async () => {
await cleanupTestDatabase();
});
Test Helpers
Database Factories:
import { createTestTenant, createTestSite } from './factories';
const tenant = await createTestTenant();
const site = await createTestSite(tenant.id);
RLS Context:
import { withTenant } from '@booking/db/rls-context';
await withTenant(testTenantId, async () => {
// Tests run within tenant context
});
API Testing
Request Testing:
import request from 'supertest';
import { app } from '../app';
describe('POST /api/v1/bookings/hold', () => {
it('should create booking hold', async () => {
const response = await request(app)
.post('/api/v1/bookings/hold')
.send(bookingDto)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
status: 'HOLD',
});
});
});
E2E Testing
E2E tests use Jest for reliable NestJS module loading and decorator support.
Setup
Full Stack Testing:
// e2e/stack.e2e.ts
import { describe, it, beforeAll, afterAll } from 'vitest';
describe('Booking E2E Flow', () => {
beforeAll(async () => {
// Start services
await startServices();
});
afterAll(async () => {
// Stop services
await stopServices();
});
});
Test Scenarios
Complete Workflows:
it('should complete booking flow', async () => {
// 1. Query availability
const availability = await queryAvailability(params);
// 2. Create hold
const hold = await createHold(holdDto);
// 3. Process payment
const payment = await processPayment(paymentDto);
// 4. Verify booking confirmed
const booking = await getBooking(hold.id);
expect(booking.status).toBe('CONFIRMED');
});
External Services
Mock External Services:
// Mock payment provider
vi.mock('../modules/payments/adapters/stripe', () => ({
StripeAdapter: vi.fn().mockImplementation(() => ({
createPayment: vi.fn().mockResolvedValue({ id: 'pi_123' }),
})),
}));
Test Data Management
Factories
Test Data Factories:
// tests/factories.ts
export async function createTestBooking(overrides = {}) {
return db().booking.create({
data: {
tenantId: 'test-tenant',
siteId: 'test-site',
productId: 'test-product',
status: 'CONFIRMED',
startsAt: new Date(),
endsAt: addHours(new Date(), 2),
...overrides,
},
});
}
Seeding
Test Seed Data:
// tests/seed.ts
export async function seedTestData() {
const tenant = await createTestTenant();
const site = await createTestSite(tenant.id);
const product = await createTestProduct(site.id);
return { tenant, site, product };
}
Cleanup
Test Cleanup:
afterEach(async () => {
// Clean up test data
await db().booking.deleteMany({ where: { tenantId: 'test-tenant' } });
});
Test Helpers
Common Utilities
Date Helpers:
import { addHours, addDays } from 'date-fns';
const tomorrow = addDays(new Date(), 1);
const twoHoursLater = addHours(new Date(), 2);
Assertion Helpers:
expect(booking).toMatchBookingShape({
status: 'CONFIRMED',
partySize: 4,
});
RLS Helpers
Tenant Context:
import { mockRlsContext } from './mock-rls-context';
beforeEach(() => {
mockRlsContext.setTenantId('test-tenant');
});
Running Tests
Development
Watch Mode:
# Watch for changes
pnpm -F api exec vitest watch
# Watch specific file
pnpm -F api exec vitest watch path/to/test.spec.ts
Single Run:
# Run all tests once
pnpm run test:api
# Run specific test file
pnpm -F api exec vitest run path/to/test.spec.ts
CI/CD
CI Test Script:
# Run tests in CI
pnpm run test:ci
This runs:
- Unit tests
- Integration tests
- E2E tests
- Coverage reports
Coverage
Generate Coverage:
pnpm -F api exec vitest run --coverage
Coverage Targets:
- Unit tests: 80%+ coverage
- Integration tests: Critical paths covered
- E2E tests: Main workflows covered
Testing Best Practices
Unit Tests
- Fast: Keep tests under 100ms
- Isolated: No external dependencies
- Deterministic: Same input = same output
- Clear: Easy to understand
- Complete: Cover edge cases
Integration Tests
- Real Dependencies: Use real database, mock external APIs
- Clean State: Reset between tests
- Realistic Data: Use realistic test data
- Error Cases: Test error handling
- Transactions: Use transactions for isolation
E2E Tests
- Critical Paths: Focus on main user journeys
- Realistic: Simulate real user behavior
- Independent: Tests don't depend on each other
- Fast Enough: Keep under reasonable time
- Maintainable: Easy to update when features change
Common Test Patterns
Testing Async Code
it('should handle async operations', async () => {
const result = await asyncFunction();
expect(result).toBeDefined();
});
Testing Error Cases
it('should throw error on invalid input', async () => {
await expect(invalidOperation()).rejects.toThrow(ValidationError);
});
Testing Database Operations
it('should create booking in database', async () => {
const booking = await createBooking(dto);
const saved = await db().booking.findUnique({ where: { id: booking.id } });
expect(saved).toBeDefined();
});
Testing Saga Patterns
it('should complete saga successfully', async () => {
const saga = await startSaga(sagaDto);
await waitForSagaCompletion(saga.id);
const completed = await getSaga(saga.id);
expect(completed.status).toBe('COMPLETED');
});
Troubleshooting
Test Failures
Common Issues:
- Database Connection: Check test database connection
- RLS Context: Ensure tenant context is set
- Async Timing: Use proper async/await
- Mock Setup: Verify mocks are configured correctly
- Data Cleanup: Ensure test data is cleaned up
Performance Issues
Slow Tests:
- Database Queries: Optimize test queries
- Test Isolation: Reduce unnecessary setup/teardown
- Parallel Execution: Run tests in parallel when possible
- Mock External Services: Don't call real external APIs in tests
Flaky Tests
Preventing Flakiness:
- Deterministic: Use fixed dates, not
new Date() - Isolation: Tests shouldn't depend on execution order
- Clean State: Reset state between tests
- Wait Conditions: Use proper waiting for async operations
Test Maintenance
Keeping Tests Updated
- Update with Features: Update tests when features change
- Refactor Tests: Keep test code clean and maintainable
- Remove Obsolete: Delete tests for removed features
- Documentation: Document complex test scenarios
Test Review
Code Review Checklist:
- Tests cover new functionality
- Edge cases tested
- Error cases tested
- Tests are fast and isolated
- Test data is realistic
- Cleanup is proper
Next Steps
- Review Setup Guide for test environment
- Check Architecture Guide for system design
- Explore Database Guide for data models
- See API Reference for endpoint testing