Testing
Test patterns for RoomKit using in-memory storage mocks
Overview
RoomKit's domain services depend on abstract storage interfaces (BookingStorage, RoomStorage, LocationStorage, …), so the cleanest way to test them is to substitute in-memory implementations of those interfaces — no Prisma client, no database.
RoomKit does not currently export a public createInMemoryStorage() factory. The mocks under packages/backend/src/__tests__/mocks/ are package-internal and not part of the published surface — copy the pattern into your own test helpers, or write a thin in-memory class per interface you actually use.
Writing your own in-memory mocks
For each storage abstract class you depend on, extend it and store data in a Map keyed by id. The internal RoomKit test suite does this and is the canonical reference (packages/backend/src/__tests__/mocks/mock-booking.storage.ts, etc. — visible in the GitHub source tree).
import { BookingStorage } from '@hfu.digital/roomkit-nestjs';
import type { Booking } from '@hfu.digital/roomkit-nestjs';
export class InMemoryBookingStorage extends BookingStorage {
private bookings = new Map<string, Booking>();
async create(booking: Booking): Promise<Booking> {
this.bookings.set(booking.id, booking);
return booking;
}
async findById(id: string): Promise<Booking | null> {
return this.bookings.get(id) ?? null;
}
// … implement the rest of the BookingStorage abstract methods …
}Compose those into a storage map and pass it to RoomKitModule.register():
import { Test } from '@nestjs/testing';
import {
RoomKitModule,
BookingService,
LocationService,
RoomService,
} from '@hfu.digital/roomkit-nestjs';
describe('BookingService', () => {
let bookingService: BookingService;
let locationService: LocationService;
let roomService: RoomService;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [
RoomKitModule.register({
storage: {
location: new InMemoryLocationStorage(),
room: new InMemoryRoomStorage(),
booking: new InMemoryBookingStorage(),
recurrence: new InMemoryRecurrenceStorage(),
blackout: new InMemoryBlackoutStorage(),
conflict: new InMemoryConflictStorage(),
config: new InMemoryConfigStorage(),
audit: new InMemoryAuditStorage(),
priority: new InMemoryPriorityStorage(),
},
}),
],
}).compile();
bookingService = module.get(BookingService);
locationService = module.get(LocationService);
roomService = module.get(RoomService);
});
});exam and bulkOperation storages are only required when you enable the corresponding features.exams / features.bulkOperations flag.
Booking lifecycle test
it('walks the full booking lifecycle', async () => {
const location = await locationService.createNode({
type: 'room',
displayName: 'Lecture Hall A',
});
const room = await roomService.create({
locationNodeId: location.id,
seatedCapacity: 100,
examCapacity: 50,
standingCapacity: 150,
});
const booking = await bookingService.create({
roomId: room.id,
requesterId: 'user-1',
title: 'CS 101 Lecture',
startsAt: new Date('2026-03-02T09:00:00Z'),
endsAt: new Date('2026-03-02T10:00:00Z'),
purposeType: 'LECTURE',
});
expect(booking.status).toBe('REQUESTED');
const confirmed = await bookingService.confirm(booking.id, 'admin');
expect(confirmed.status).toBe('CONFIRMED');
const started = await bookingService.checkIn(booking.id, 'user-1');
expect(started.status).toBe('IN_PROGRESS');
const completed = await bookingService.complete(booking.id, 'user-1');
expect(completed.status).toBe('COMPLETED');
});BookingStatus is the RecurrenceFrequency / BookingStatus union exported from ./types/enums — REQUESTED | CONFIRMED | IN_PROGRESS | COMPLETED | CANCELLED.
Conflict detection test
import { BookingConflictError } from '@hfu.digital/roomkit-nestjs';
it('detects overlapping bookings', async () => {
const booking = await bookingService.create({
roomId: room.id,
requesterId: 'user-1',
title: 'Morning Meeting',
startsAt: new Date('2026-03-02T09:00:00Z'),
endsAt: new Date('2026-03-02T10:00:00Z'),
purposeType: 'SEMINAR',
});
await expect(
bookingService.create({
roomId: room.id,
requesterId: 'user-2',
title: 'Overlapping Meeting',
startsAt: new Date('2026-03-02T09:30:00Z'),
endsAt: new Date('2026-03-02T10:30:00Z'),
purposeType: 'SEMINAR',
}),
).rejects.toThrow(BookingConflictError);
});Recurrence test
import { RecurrenceService } from '@hfu.digital/roomkit-nestjs';
it('expands a weekly series', async () => {
const recurrenceService = module.get(RecurrenceService);
const result = await recurrenceService.createRecurringBooking(
{
roomId: room.id,
requesterId: 'user-1',
title: 'Weekly Standup',
startsAt: new Date('2026-03-02T09:00:00Z'),
endsAt: new Date('2026-03-02T09:30:00Z'),
purposeType: 'SEMINAR',
},
{
frequency: 'WEEKLY',
seriesStartsAt: new Date('2026-03-02'),
seriesEndsAt: new Date('2026-03-30'),
daysOfWeek: [1], // Monday
},
);
// Mar 2, 9, 16, 23, 30 — five Mondays in the window
expect(result.bookings).toHaveLength(5);
});Note the public method is createRecurringBooking, not createRecurringSeries. See the RecurrenceService reference.
Testing emitted events
import { EventBus } from '@hfu.digital/roomkit-nestjs';
it('emits BookingRequested on create', async () => {
const eventBus = module.get(EventBus);
const events: any[] = [];
eventBus.on('BookingRequested', (event) => events.push(event));
const booking = await bookingService.create({
roomId: room.id,
requesterId: 'user-1',
title: 'Event Test',
startsAt: new Date('2026-03-02T14:00:00Z'),
endsAt: new Date('2026-03-02T15:00:00Z'),
purposeType: 'SEMINAR',
});
expect(events).toHaveLength(1);
expect(events[0].payload.id).toBe(booking.id);
});