Testing - Mocking & Configuration
Mocking Best Practices
Section titled “Mocking Best Practices”To keep tests clean and maintainable, we use Mock Factories.
What: A file in test/mocks/mock-[name].ts that exports functions to create mocks.
Why:
- DRY (Don’t Repeat Yourself): Re-use the same mock structure in Unit and E2E tests
- Centralized: If an entity changes, you only update the mock in one place
- Clean Tests: Your test files focus on logic (arrange, act, assert) instead of setup (building large mock objects)
- Type Safety: Export mock types for better IntelliSense and compile-time checking
Mock Service Factory Example
Section titled “Mock Service Factory Example”This factory is the standard pattern for our project.
import { CreateEngagementOrderDTO } from '../../src/modules/engagement/dto/create-engagement-order.dto';import { UpdateEngagementOrderDTO } from '../../src/modules/engagement/dto/update-engagement-order.dto';import { EngagementOrder } from '../../src/modules/engagement/entities/engagement-order.entity';
/** * Export the Type for easy use in tests * This provides IntelliSense and type checking */export type MockEngagementOrdersService = { createEngagementOrder: jest.Mock; update: jest.Mock; findPaginated: jest.Mock; findById: jest.Mock; delete: jest.Mock;};
/** * Helper to create a "fake" entity with sensible defaults * @param overrides - Partial entity to override defaults */function createMockEngagementOrderEntity(overrides: Partial<EngagementOrder> = {}): EngagementOrder { const defaultEntity: EngagementOrder = { id: '550e8400-e29b-41d4-a716-446655440002', engagement_id: '550e8400-e29b-41d4-a716-446655440000', order_number: 'ORD-2025-0001', order_type: 'LAB', order_code: 'LAB001', order_name: 'Complete Blood Count', quantity: 1, priority: 'ROUTINE', status: 'PENDING', created_at: new Date(), updated_at: new Date(), created_by: 'user-uuid-123', updated_by: 'user-uuid-123', } as EngagementOrder;
return { ...defaultEntity, ...overrides };}
/** * Helper to create a "fake" DTO with sensible defaults * @param engagementId - Engagement ID to associate with the order * @param overrides - Partial DTO to override defaults */export function createMockEngagementOrderDTO( engagementId: string, overrides: Partial<CreateEngagementOrderDTO> = {},): CreateEngagementOrderDTO { const defaultDTO: CreateEngagementOrderDTO = { order_type: 'LAB', order_code: 'LAB001', order_name: 'Complete Blood Count', quantity: 1, priority: 'ROUTINE', } as CreateEngagementOrderDTO;
return { ...defaultDTO, ...overrides };}
/** * The main factory function * Creates a mock service with all methods pre-configured */export function createMockEngagementOrdersService(): MockEngagementOrdersService { const defaultMockEntity = createMockEngagementOrderEntity();
return { // Mock implementation returns dynamic entity based on inputs createEngagementOrder: jest.fn().mockImplementation((engagementId, dto, user) => Promise.resolve( createMockEngagementOrderEntity({ engagement_id: engagementId, ...dto, created_by: user?.id, updated_by: user?.id, }), ), ), update: jest.fn().mockImplementation((id, dto, user) => Promise.resolve( createMockEngagementOrderEntity({ id, ...dto, updated_by: user?.id, }), ), ), findPaginated: jest.fn().mockResolvedValue({ data: [defaultMockEntity], pagination: { page: 1, page_size: 10, total: 1, total_pages: 1 }, }), findById: jest.fn().mockResolvedValue(defaultMockEntity), delete: jest.fn().mockResolvedValue(undefined), };}Usage in Tests
Section titled “Usage in Tests”// In unit testconst mockService = createMockEngagementOrdersService();
// Customize mock behavior for specific testmockService.createEngagementOrder.mockResolvedValue(createMockEngagementOrderEntity({ status: 'COMPLETED' }));
// In E2E testconst mockDTO = createMockEngagementOrderDTO(MOCK_ENGAGEMENT_ID, { priority: 'URGENT',});Jest Configuration
Section titled “Jest Configuration”Unit Test Configuration (jest.config.js)
Section titled “Unit Test Configuration (jest.config.js)”module.exports = { displayName: 'data-owner-bc:unit', moduleFileExtensions: ['js', 'json', 'ts'], rootDir: '.', testEnvironment: 'node', testMatch: ['<rootDir>/test/unit/**/*.spec.ts'], coverageDirectory: '../../coverage/data-owner-bc/unit', collectCoverageFrom: [ 'src/**/*.(t|j)s', '!src/main.ts', '!src/**/*.module.ts', '!src/**/*.dto.ts', '!src/**/*.entity.ts', '!src/**/*.enum.ts', '!src/**/*.interface.ts', ], transform: { '^.+\\.(t|j)s$': 'ts-jest', }, transformIgnorePatterns: ['/node_modules/(?!uuid)'], moduleNameMapper: { '^@lib/common(|/.*)$': '<rootDir>/../../libs/common/src/$1', '^@lib/config(|/.*)$': '<rootDir>/../../libs/config/src/$1', '^@lib/database(|/.*)$': '<rootDir>/../../libs/database/src/$1', '^apps/data-owner-bc(|/.*)$': '<rootDir>/$1', },};E2E Test Configuration (test/jest-e2e.json)
Section titled “E2E Test Configuration (test/jest-e2e.json)”{ "displayName": "data-owner-bc:e2e", "moduleFileExtensions": ["js", "json", "ts"], "rootDir": ".", "testEnvironment": "node", "testMatch": ["<rootDir>/e2e/**/*.e2e-spec.ts"], "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "transformIgnorePatterns": ["/node_modules/(?!uuid)"], "moduleNameMapper": { "^@lib/common(|/.*)$": "<rootDir>/../../../libs/common/src/$1", "^@lib/config(|/.*)$": "<rootDir>/../../../libs/config/src/$1", "^@lib/database(|/.*)$": "<rootDir>/../../../libs/database/src/$1", "^@apps/data-owner-bc(|/.*)$": "<rootDir>/../$1", "^apps/data-owner-bc(|/.*)$": "<rootDir>/../$1", "^@apps/(.*)$": "<rootDir>/../../$1" }, "collectCoverageFrom": ["**/*.(t|j)s"], "coverageDirectory": "../coverage/e2e", "coveragePathIgnorePatterns": [ "/node_modules/", "main.ts$", ".module.ts$", ".dto.ts$", ".enum.ts$", ".interface.ts$" ]}Package.json Scripts
Section titled “Package.json Scripts”{ "scripts": { "test:data-owner-bc": "cross-env NODE_ENV=dev jest --config ./apps/data-owner-bc/jest.config.js --watch", "test:data-owner-bc:e2e": "cross-env NODE_ENV=dev jest --config ./apps/data-owner-bc/test/jest-e2e.json --watch" }}Best Practices
Section titled “Best Practices”1. Test Organization
Section titled “1. Test Organization”describe('EngagementOrdersController', () => { // Group related tests using describe blocks describe('createEngagementOrder', () => { it('should create order with valid data', () => {}); it('should reject invalid data', () => {}); it('should handle duplicate order code', () => {}); });
describe('updateEngagementOrder', () => { it('should update existing order', () => {}); it('should return 404 for non-existent order', () => {}); });});2. Descriptive Test Names
Section titled “2. Descriptive Test Names”// ❌ Bad - vagueit('should work', () => {});
// ✅ Good - descriptiveit('should return 400 when order_type is missing', () => {});it('should create order with valid priority', () => {});it('should soft delete order and preserve audit data', () => {});3. Test One Thing
Section titled “3. Test One Thing”// ❌ Bad - testing multiple thingsit('should create and update order', async () => { const created = await controller.create(dto); const updated = await controller.update(created.id, updateDTO); // ...});
// ✅ Good - separate testsit('should create order', async () => { const result = await controller.create(dto); expect(result).toBeDefined();});
it('should update order', async () => { const result = await controller.update(id, updateDTO); expect(result.status).toBe(updateDTO.status);});4. Use beforeEach/beforeAll for Setup
Section titled “4. Use beforeEach/beforeAll for Setup”describe('EngagementOrdersService', () => { let service: EngagementOrdersService; let mockData: EngagementOrder;
beforeEach(() => { // Reset test data before each test mockData = createMockEngagementOrderEntity(); });
beforeAll(async () => { // One-time setup (module creation) const module = await createTestingModule(...); service = module.get(EngagementOrdersService); });
// Tests use fresh mockData each time});5. Clean Up After Tests
Section titled “5. Clean Up After Tests”afterEach(() => { jest.clearAllMocks(); // Clear mock call history});
afterAll(async () => { await app.close(); // Close app in E2E tests});6. Test Error Cases
Section titled “6. Test Error Cases”describe('findOne', () => { it('should return order when found', async () => { mockService.findOne.mockResolvedValue(mockOrder); const result = await controller.findOne('123'); expect(result).toEqual(mockOrder); });
it('should throw NotFoundException when order not found', async () => { mockService.findOne.mockRejectedValue(new NotFoundException()); await expect(controller.findOne('invalid-id')).rejects.toThrow(NotFoundException); });});7. Test Validation in E2E
Section titled “7. Test Validation in E2E”// E2E test for DTO validationit('should return 400 for invalid order_type', async () => { const invalidDTO = { order_type: 'INVALID', // Invalid enum value order_code: 'LAB001', };
await request(app.getHttpServer()).post('/engagements/123/orders').send(invalidDTO).expect(400);});Troubleshooting
Section titled “Troubleshooting”1. Pipes or Interceptors Not Working in E2E Tests
Section titled “1. Pipes or Interceptors Not Working in E2E Tests”Problem: Your E2E tests (using request) return a 201 but the response body is raw data (not JSON:API format). Or, invalid DTOs are passing validation and returning 201 instead of 400.
Cause: Your test application created with createTestingModule does not automatically register global pipes or interceptors defined in main.ts.
Solution: Ensure your shared helper createTestApp (from @lib/common/testing) is correctly registering all global utilities.
Example (libs/common/src/testing/test-setup.ts):
import { INestApplication } from '@nestjs/common';import { Reflector } from '@nestjs/core';import { Test, TestingModule } from '@nestjs/testing';
import { AllExceptionsFilter } from '@lib/common/utils/http-exception/all-exceptions-filter.util';import { TransformInterceptor } from '@lib/common/utils/http-success/transform-interceptor.util';
/** * Creates a fully configured test application * Applies the same global pipes, interceptors, and filters as production */export async function createTestApp(moduleFixture: TestingModule): Promise<INestApplication> { const app = moduleFixture.createNestApplication();
// 1. Apply the same validation pipe as production app.useGlobalPipes( new ValidationPipe({ whitelist: true, // Strip unknown properties transform: true, // Transform to DTO instances forbidNonWhitelisted: true, // Throw error on unknown properties }), );
// 2. Apply the same transform interceptor as production const reflector = app.get(Reflector); // Interceptor needs Reflector app.useGlobalInterceptors(new TransformInterceptor(reflector));
// 3. Apply the same exception filter as production app.useGlobalFilters(new AllExceptionsFilter());
await app.init(); return app;}
/** * Creates a testing module with required providers */export async function createTestingModule( controllers: any[], providers: any[] = [],): Promise<TestingModule> { // Ensure Reflector is available for the app to .get() const requiredProviders = [Reflector];
return Test.createTestingModule({ controllers, providers: [...requiredProviders, ...providers], }).compile();}2. Module Import Errors (Cannot find module ‘@lib/common’)
Section titled “2. Module Import Errors (Cannot find module ‘@lib/common’)”Problem: Jest can’t find your monorepo library aliases.
Solution: Ensure your jest.config.js and test/jest-e2e.json have the correct moduleNameMapper paths.
moduleNameMapper: { '^@lib/common(|/.*)$': '<rootDir>/../../libs/common/src/$1', '^@lib/config(|/.*)$': '<rootDir>/../../libs/config/src/$1', '^@lib/database(|/.*)$': '<rootDir>/../../libs/database/src/$1',}3. Async Test Timeout
Section titled “3. Async Test Timeout”Problem: Timeout - Async callback was not invoked within the 5000 ms timeout
Solution: Ensure all promises are awaited. In unit tests, make sure you use mockResolvedValue or mockRejectedValue for async service methods, not mockReturnValue.
// ❌ Bad (for async function)mockService.create.mockReturnValue(mockOrder);
// ✅ GoodmockService.create.mockResolvedValue(mockOrder);4. Mocks Not Clearing
Section titled “4. Mocks Not Clearing”// Mock still has data from previous testexpect(mockService.method).toHaveBeenCalledTimes(1); // Fails with 2
// Solution: Clear mocks in afterEachafterEach(() => { jest.clearAllMocks();});5. ValidationPipe Not Working in E2E Tests
Section titled “5. ValidationPipe Not Working in E2E Tests”// E2E test expects 400 but gets 201
// Solution: Ensure ValidationPipe is applied in createTestAppapp.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidNonWhitelisted: true, }),);