Skip to content

Testing - Mocking & Configuration

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:

  1. DRY (Don’t Repeat Yourself): Re-use the same mock structure in Unit and E2E tests
  2. Centralized: If an entity changes, you only update the mock in one place
  3. Clean Tests: Your test files focus on logic (arrange, act, assert) instead of setup (building large mock objects)
  4. Type Safety: Export mock types for better IntelliSense and compile-time checking

This factory is the standard pattern for our project.

apps/data-owner-bc/test/mocks/mock-engagement-orders.ts
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),
};
}
// In unit test
const mockService = createMockEngagementOrdersService();
// Customize mock behavior for specific test
mockService.createEngagementOrder.mockResolvedValue(createMockEngagementOrderEntity({ status: 'COMPLETED' }));
// In E2E test
const mockDTO = createMockEngagementOrderDTO(MOCK_ENGAGEMENT_ID, {
priority: 'URGENT',
});

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$"
]
}
{
"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"
}
}

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', () => {});
});
});
// ❌ Bad - vague
it('should work', () => {});
// ✅ Good - descriptive
it('should return 400 when order_type is missing', () => {});
it('should create order with valid priority', () => {});
it('should soft delete order and preserve audit data', () => {});
// ❌ Bad - testing multiple things
it('should create and update order', async () => {
const created = await controller.create(dto);
const updated = await controller.update(created.id, updateDTO);
// ...
});
// ✅ Good - separate tests
it('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);
});
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
});
afterEach(() => {
jest.clearAllMocks(); // Clear mock call history
});
afterAll(async () => {
await app.close(); // Close app in E2E tests
});
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);
});
});
// E2E test for DTO validation
it('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);
});

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',
}

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);
// ✅ Good
mockService.create.mockResolvedValue(mockOrder);
// Mock still has data from previous test
expect(mockService.method).toHaveBeenCalledTimes(1); // Fails with 2
// Solution: Clear mocks in afterEach
afterEach(() => {
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 createTestApp
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);