Skip to main content

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.ts or in tests/integration/
  • E2E Tests: *.e2e.ts or in e2e/

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-jest and emitDecoratorMetadata: true for 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

  1. Isolate Dependencies: Mock external services
  2. Test Edge Cases: Boundary conditions, errors
  3. Fast Execution: Keep tests under 100ms
  4. Clear Names: Descriptive test names
  5. 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

  1. Fast: Keep tests under 100ms
  2. Isolated: No external dependencies
  3. Deterministic: Same input = same output
  4. Clear: Easy to understand
  5. Complete: Cover edge cases

Integration Tests

  1. Real Dependencies: Use real database, mock external APIs
  2. Clean State: Reset between tests
  3. Realistic Data: Use realistic test data
  4. Error Cases: Test error handling
  5. Transactions: Use transactions for isolation

E2E Tests

  1. Critical Paths: Focus on main user journeys
  2. Realistic: Simulate real user behavior
  3. Independent: Tests don't depend on each other
  4. Fast Enough: Keep under reasonable time
  5. 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:

  1. Database Connection: Check test database connection
  2. RLS Context: Ensure tenant context is set
  3. Async Timing: Use proper async/await
  4. Mock Setup: Verify mocks are configured correctly
  5. Data Cleanup: Ensure test data is cleaned up

Performance Issues

Slow Tests:

  1. Database Queries: Optimize test queries
  2. Test Isolation: Reduce unnecessary setup/teardown
  3. Parallel Execution: Run tests in parallel when possible
  4. Mock External Services: Don't call real external APIs in tests

Flaky Tests

Preventing Flakiness:

  1. Deterministic: Use fixed dates, not new Date()
  2. Isolation: Tests shouldn't depend on execution order
  3. Clean State: Reset state between tests
  4. Wait Conditions: Use proper waiting for async operations

Test Maintenance

Keeping Tests Updated

  1. Update with Features: Update tests when features change
  2. Refactor Tests: Keep test code clean and maintainable
  3. Remove Obsolete: Delete tests for removed features
  4. 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