Testing Guide
This guide covers testing practices and standards for HMP development.
Testing Philosophy
- Write tests for all new features
- Maintain high test coverage (>80%)
- Tests should be readable and maintainable
- Test behavior, not implementation
Test Types
Unit Tests
Test individual functions and classes in isolation.
// services/reservation-service.test.ts
describe('ReservationService', () => {
let service: ReservationService;
let mockRepository: jest.Mocked<ReservationRepository>;
let mockEventBus: jest.Mocked<EventBus>;
beforeEach(() => {
mockRepository = createMockRepository();
mockEventBus = createMockEventBus();
service = new ReservationService(mockRepository, mockEventBus);
});
describe('create', () => {
it('should create a reservation with valid data', async () => {
const input = {
guestId: 'guest_123',
arrivalDate: new Date('2026-02-15'),
departureDate: new Date('2026-02-18'),
rooms: [{ roomTypeId: 'rt_deluxe', adults: 2 }],
};
mockRepository.create.mockResolvedValue({
id: 'res_123',
status: 'CONFIRMED',
...input,
});
const result = await service.create(input);
expect(result.id).toBe('res_123');
expect(result.status).toBe('CONFIRMED');
expect(mockRepository.create).toHaveBeenCalledWith(input);
expect(mockEventBus.publish).toHaveBeenCalledWith(
'reservation.created',
expect.any(Object),
);
});
it('should throw error when no availability', async () => {
mockRepository.checkAvailability.mockResolvedValue(false);
await expect(service.create(validInput)).rejects.toThrow(
NoAvailabilityError,
);
});
});
});
Integration Tests
Test multiple components working together.
// tests/integration/reservations.test.ts
describe('Reservations API', () => {
let app: Express;
let db: Database;
beforeAll(async () => {
db = await setupTestDatabase();
app = createApp(db);
});
afterAll(async () => {
await db.close();
});
beforeEach(async () => {
await db.clear();
await seedTestData(db);
});
describe('POST /api/v1/reservations', () => {
it('should create a reservation', async () => {
const response = await request(app)
.post('/api/v1/reservations')
.set('Authorization', `Bearer ${testToken}`)
.set('X-Property-ID', 'prop_test')
.send({
guest_id: 'guest_123',
arrival_date: '2026-02-15',
departure_date: '2026-02-18',
rooms: [{ room_type_id: 'rt_deluxe', adults: 2 }],
});
expect(response.status).toBe(201);
expect(response.body.data).toMatchObject({
status: 'CONFIRMED',
guest: { id: 'guest_123' },
});
// Verify in database
const reservation = await db.reservation.findUnique({
where: { id: response.body.data.id },
});
expect(reservation).not.toBeNull();
});
it('should return 400 for invalid dates', async () => {
const response = await request(app)
.post('/api/v1/reservations')
.set('Authorization', `Bearer ${testToken}`)
.set('X-Property-ID', 'prop_test')
.send({
guest_id: 'guest_123',
arrival_date: '2026-02-20',
departure_date: '2026-02-18', // Before arrival
rooms: [{ room_type_id: 'rt_deluxe', adults: 2 }],
});
expect(response.status).toBe(400);
expect(response.body.error).toBe('validation_error');
});
});
});
End-to-End Tests
Test complete user flows.
// tests/e2e/check-in-flow.test.ts
describe('Guest Check-In Flow', () => {
it('should complete full check-in process', async () => {
// Create reservation
const reservation = await createTestReservation({
arrivalDate: today(),
status: 'CONFIRMED',
});
// Login as front desk agent
const session = await login('frontdesk@hotel.com', 'password');
// Navigate to arrivals
const arrivals = await session.get('/api/v1/front-desk/arrivals');
expect(arrivals.body.data).toContainEqual(
expect.objectContaining({ id: reservation.id }),
);
// Start check-in
const checkIn = await session.post(
`/api/v1/front-desk/check-in/${reservation.id}`,
{
room_id: 'room_405',
id_document: {
type: 'PASSPORT',
number: 'J1234567',
},
},
);
expect(checkIn.status).toBe(200);
expect(checkIn.body.data.status).toBe('CHECKED_IN');
// Verify room status updated
const room = await session.get('/api/v1/rooms/room_405');
expect(room.body.data.fo_status).toBe('OCCUPIED');
});
});
Test Utilities
Factory Functions
// tests/factories/reservation.factory.ts
export function createTestReservation(
overrides: Partial<Reservation> = {},
): Reservation {
return {
id: `res_${randomId()}`,
confirmationNumber: `TEST${Date.now()}`,
status: 'CONFIRMED',
guestId: 'guest_test',
arrivalDate: addDays(new Date(), 7),
departureDate: addDays(new Date(), 10),
totalAmount: 15000,
...overrides,
};
}
export function createTestGuest(overrides: Partial<Guest> = {}): Guest {
return {
id: `guest_${randomId()}`,
firstName: 'Test',
lastName: 'Guest',
email: `test_${randomId()}@example.com`,
...overrides,
};
}
Mock Utilities
// tests/mocks/repository.mock.ts
export function createMockRepository<T>(): jest.Mocked<Repository<T>> {
return {
findById: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
}
// tests/mocks/event-bus.mock.ts
export function createMockEventBus(): jest.Mocked<EventBus> {
return {
publish: jest.fn().mockResolvedValue(undefined),
subscribe: jest.fn(),
};
}
Test Database
// tests/setup/database.ts
export async function setupTestDatabase(): Promise<Database> {
const db = new Database({
url: process.env.TEST_DATABASE_URL,
});
await db.migrate();
return db;
}
export async function seedTestData(db: Database): Promise<void> {
// Create test property
await db.property.create({
data: {
id: 'prop_test',
code: 'TEST',
name: 'Test Hotel',
},
});
// Create room types
await db.roomType.createMany({
data: [
{ id: 'rt_standard', code: 'STD', name: 'Standard' },
{ id: 'rt_deluxe', code: 'DLX', name: 'Deluxe' },
],
});
// Create test rooms
await db.room.createMany({
data: [
{ id: 'room_101', roomNumber: '101', roomTypeId: 'rt_standard' },
{ id: 'room_405', roomNumber: '405', roomTypeId: 'rt_deluxe' },
],
});
}
Running Tests
Commands
# Run all tests
npm test
# Run unit tests only
npm run test:unit
# Run integration tests
npm run test:integration
# Run e2e tests
npm run test:e2e
# Run with coverage
npm run test:coverage
# Run specific test file
npm test -- reservation-service.test.ts
# Run tests matching pattern
npm test -- --grep "create reservation"
# Watch mode
npm test -- --watch
Configuration
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
Testing Best Practices
Arrange-Act-Assert Pattern
it('should calculate correct total', () => {
// Arrange
const reservation = createTestReservation({
nights: 3,
ratePerNight: 5000,
});
// Act
const total = calculateTotal(reservation);
// Assert
expect(total).toBe(15000);
});
Test Isolation
// DO: Each test is independent
describe('ReservationService', () => {
beforeEach(() => {
// Fresh setup for each test
service = new ReservationService(mockRepo);
});
it('test 1', () => { });
it('test 2', () => { });
});
// DON'T: Tests depend on each other
let reservation;
it('creates reservation', () => {
reservation = service.create(data);
});
it('updates reservation', () => {
service.update(reservation.id, newData); // Fails if first test fails
});
Descriptive Test Names
// DO: Describe expected behavior
it('should throw NoAvailabilityError when all rooms are booked', () => {});
it('should apply early bird discount for bookings 30+ days ahead', () => {});
// DON'T: Vague names
it('works', () => {});
it('test create', () => {});
Test Edge Cases
describe('calculateRate', () => {
it('should handle single night stays', () => {});
it('should handle maximum length stays', () => {});
it('should handle zero amount', () => {});
it('should handle decimal amounts', () => {});
it('should handle date boundary (midnight)', () => {});
});
Mocking Guidelines
When to Mock
- External services (APIs, databases in unit tests)
- Time-dependent functions
- Random values
- System resources (file system, network)
When Not to Mock
- Pure functions
- Simple data transformations
- Internal collaborators (in integration tests)
Mocking Examples
// Mock external API
jest.mock('../services/payment-gateway', () => ({
processPayment: jest.fn().mockResolvedValue({ success: true }),
}));
// Mock time
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-02-15'));
// Mock random
jest.spyOn(Math, 'random').mockReturnValue(0.5);