Testing (Unit & E2E)
Overview
Section titled βOverviewβThis guide provides comprehensive instructions for writing unit tests and end-to-end (E2E) tests for NestJS microservices using Jest and Supertest. Testing is critical in an enterprise system to ensure reliability, correctness, and maintainability.
Coding Constants & Lint Rules
Section titled βCoding Constants & Lint RulesβTo ensure code consistency and compliance with our strict ESLint configuration, all generated code (including tests) MUST adhere to the following rules:
-
Explicit Types Only:
- NEVER use
any. Always define explicit types or interfaces. - β
const data: any = ... - β
const data: UserProfile = ...
- NEVER use
-
Naming Conventions (Strictly Enforced):
- Variables/Functions:
camelCase - Classes/Interfaces:
PascalCase - Enum Members:
UPPER_CASE - DTO/Entity Properties:
snake_case(Critically important for API consistency)- β
firstName: string; - β
first_name: string;
- β
- Boolean Variables: Prefix with verbs like
is,has,should(e.g.,isValid,hasPermission).
- Variables/Functions:
-
Strict Boolean Checks:
- Do not rely on implicit truthy/falsy checks for objects, numbers, or strings.
- β
if (users.length) ... - β
if (users.length > 0) ... - β
if (user) ... - β
if (user !== null) ...
-
Testing Standards:
- No
console.log: usage in tests is prohibited. - Mocking: Use
createMock<Service>()or dedicated mock factories. Avoidjest.fn() as any. - Isolation: Tests must not depend on external state or execution order.
- No
-
Clean Code:
- Remove unused variables and imports.
- Use
constby default,letonly when necessary. - Functions should have explicit return types.
Testing Philosophy
Section titled βTesting PhilosophyβWhy We Test:
- Reliability: Business data must be handled correctly
- Regression Prevention: Ensure new features donβt break existing functionality
- Documentation: Tests serve as executable documentation
- Refactoring Confidence: Safely improve code structure with a safety net
- Early Bug Detection: Catch issues in isolated components before they integrate
The Testing Pyramid:
/\ / \ E2E Tests (Few) /____\ - Test full workflows (HTTP, Pipes, Interceptors) / \ - Slower, but prove integration / Unit \ Integration Tests (Some) / Tests \ - Test module/service interactions /____________\ Unit Tests (Many) - Test a single function/class in isolation - Fast, focused, and predictableProject Testing Structure
Section titled βProject Testing StructureβEach microservice follows this standardized testing structure. Note the use of the test/mocks directory to store reusable mock factories.
apps/[SERVICE_NAME]/βββ jest.config.js # Unit test configurationβββ src/β βββ modules/β βββ [MODULE]/β βββ controllers/β β βββ [name].controller.tsβ βββ services/β β βββ [name].service.tsβ βββ entities/β β βββ [name].entity.tsβ βββ dto/β βββ create-[name].dto.tsβ βββ update-[name].dto.tsβββ test/ βββ jest-e2e.json # E2E test configuration βββ mocks/ # (Plural) Reusable mock factories β βββ mock-[name].ts βββ unit/ # Unit tests (controller & service) β βββ [name].controller.spec.ts β βββ [name].service.spec.ts βββ e2e/ # End-to-end tests βββ [name].e2e-spec.tsUnified Testing Standards (Best Practices)
Section titled βUnified Testing Standards (Best Practices)βTo ensure consistency across data-owner-bc, data-consumer-bc, and future services, we follow these patterns:
-
Mock Factory Pattern (Recommended):
- Avoid defining large mock objects directly inside
.spec.tsfiles. - Create factory functions in
test/mocks/mock-[name].ts. - Naming:
createMock[Name]Servicefor service mocks,mock[Name]Entityfor data.
- Avoid defining large mock objects directly inside
-
Shared Testing Utilities:
- Use
createTestingModuleandcreateTestAppfrom@lib/common/testing. - This ensures that Global Pipes, Interceptors, and Filters are automatically loaded (especially critical for E2E tests).
- Use
-
Encapsulated Constants:
- Export common UUIDs and test values from the mock factory (e.g.,
MOCK_USER_ID,MOCK_RESOURCE_ID). - This makes tests readable and easy to update.
- Export common UUIDs and test values from the mock factory (e.g.,
-
Mock Interaction Verification:
- Always verify that the service was called with the correct arguments.
- Example:
expect(service.create).toHaveBeenCalledWith(dto, userSession);
-
Clean State:
- Always run
jest.clearAllMocks()inafterEach()to avoid cross-test pollution.
- Always run
Quick Start
Section titled βQuick Startβ# Run all testsnpm run test
# Run tests for specific service (watch mode)npm run test:data-owner-bc # Unit testsnpm run test:data-owner-bc:e2e # E2E tests
# Run with coveragenpm run test:cov
# Run specific test filenpm run test -- engagement-orders.controller.spec.tsUnit Testing Deep Dive
Section titled βUnit Testing Deep DiveβWhat is Unit Testing?
Section titled βWhat is Unit Testing?βUnit testing tests individual components (e.g., a single Controller or Service) in complete isolation. All external dependencies (other services, databases, APIs) are replaced with mocks.
Key Principles:
- Isolation: Test one class at a time
- Fast: No network, database, or file system calls
- Predictable: Same input always produces same output
- Independent: Tests donβt affect each other
- Mock Dependencies: The Service is mocked when testing the Controller. The Repository/EntityManager is mocked when testing the Service.
Unit Test Structure
Section titled βUnit Test StructureβEvery unit test follows the AAA pattern:
it('should do something', async () => { // 1. ARRANGE - Set up test data and mocks const input = { name: 'John' }; mockService.method.mockResolvedValue({ id: '123', name: 'John' });
// 2. ACT - Execute the code under test const result = await controller.method(input);
// 3. ASSERT - Verify the results expect(result).toEqual({ id: '123', name: 'John' }); expect(mockService.method).toHaveBeenCalledWith(input);});Controller Unit Test Template
Section titled βController Unit Test TemplateβControllers are the HTTP entry point and should delegate business logic to services. Controller unit tests verify only the controllerβs logic. They do not test validation pipes or interceptors.
Test Goals:
- Does the controller method call the correct service method?
- Does it pass the correct arguments (DTO, params, user) to the service?
- Does it return the value that the service provides?
- Does it handle errors appropriately?
Complete Example:
This template uses our monorepoβs shared createTestingModule helper and a createMock...Service factory.
import { Test, TestingModule } from '@nestjs/testing';
import { type IUserSession } from '@lib/common';import { createTestingModule } from '@lib/common/testing';
import { createMockEngagementOrdersService } from '@apps/data-owner-bc/test/mocks/mock-engagement-orders'; // 1. Import mock factory
import { EngagementOrdersController } from '../../src/modules/engagement/controllers/engagement-orders.controller';import { CreateEngagementOrderDTO } from '../../src/modules/engagement/dto/create-engagement-order.dto';import { EngagementOrder } from '../../src/modules/engagement/entities/engagement-order.entity';import { EngagementOrdersService } from '../../src/modules/engagement/services/engagement-orders.service';
describe('EngagementOrdersController (Unit)', () => { let controller: EngagementOrdersController; let service: EngagementOrdersService;
const mockCurrentUser: IUserSession = { id: 'user-uuid-123', username: 'testuser', email: 'test@example.com', roles: ['admin'], permissions: ['engagement:create', 'engagement:update'], fullname: 'Test User', };
const mockEngagementOrder: EngagementOrder = { id: 'order-uuid-123', engagement_id: 'engagement-uuid-123', order_type: 'LAB', status: 'PENDING', created_at: new Date(), updated_at: new Date(), } as EngagementOrder;
// 2. Create the mock service instance const mockEngagementOrdersService = createMockEngagementOrdersService();
beforeAll(async () => { // 3. Use the shared createTestingModule helper const module: TestingModule = await createTestingModule( [EngagementOrdersController], // Controller(s) under test [ { // Provider(s) to be mocked provide: EngagementOrdersService, useValue: mockEngagementOrdersService, }, ], ).compile();
// 4. Get the "real" controller and "fake" service controller = module.get<EngagementOrdersController>(EngagementOrdersController); service = module.get<EngagementOrdersService>(EngagementOrdersService); });
afterEach(() => { jest.clearAllMocks(); });
it('should be defined', () => { expect(controller).toBeDefined(); expect(service).toBeDefined(); });
describe('createEngagementOrder', () => { it('should create a new engagement order successfully', async () => { // ARRANGE const engagementId = 'engagement-uuid-123'; const createDTO: CreateEngagementOrderDTO = { order_type: 'LAB', order_code: 'LAB001', order_name: 'Complete Blood Count', quantity: 1, priority: 'ROUTINE', } as any;
// Setup the mock's return value mockEngagementOrdersService.createEngagementOrder.mockResolvedValue(mockEngagementOrder);
// ACT // Call the controller method directly const result = await controller.createEngagementOrder(engagementId, createDTO, mockCurrentUser);
// ASSERT expect(result).toEqual(mockEngagementOrder); // 1. Did it return the service's value? expect(service.createEngagementOrder).toHaveBeenCalledWith( // 2. Was the service called correctly? engagementId, createDTO, mockCurrentUser, ); expect(service.createEngagementOrder).toHaveBeenCalledTimes(1); });
it('should handle service errors', async () => { // ARRANGE const error = new Error('Engagement not found'); mockEngagementOrdersService.createEngagementOrder.mockRejectedValue(error);
// ACT & ASSERT await expect( controller.createEngagementOrder('invalid-id', {} as any, mockCurrentUser), ).rejects.toThrow('Engagement not found');
expect(service.createEngagementOrder).toHaveBeenCalledWith( 'invalid-id', expect.any(Object), mockCurrentUser, ); }); });
describe('updateEngagementOrder', () => { it('should update an existing order successfully', async () => { const engagementId = 'engagement-uuid-123'; const orderId = 'order-uuid-123'; const updateDTO = { status: 'COMPLETED', };
const updatedOrder = { ...mockEngagementOrder, ...updateDTO }; mockEngagementOrdersService.update.mockResolvedValue(updatedOrder);
const result = await controller.updateEngagementOrder( engagementId, orderId, updateDTO, mockCurrentUser, );
expect(result).toEqual(updatedOrder); expect(service.update).toHaveBeenCalledWith(orderId, updateDTO, mockCurrentUser); expect(service.update).toHaveBeenCalledTimes(1); });
it('should handle order not found error', async () => { const error = new Error('Order not found'); mockEngagementOrdersService.update.mockRejectedValue(error);
await expect( controller.updateEngagementOrder('engagement-id', 'invalid-id', {}, mockCurrentUser), ).rejects.toThrow('Order not found'); }); });
describe('findOne', () => { it('should return a single order by ID', async () => { const engagementId = 'engagement-uuid-123'; const orderId = 'order-uuid-123'; mockEngagementOrdersService.findById.mockResolvedValue(mockEngagementOrder);
const result = await controller.findOne(engagementId, orderId);
expect(result).toEqual(mockEngagementOrder); expect(service.findById).toHaveBeenCalledWith(orderId); expect(service.findById).toHaveBeenCalledTimes(1); });
it('should handle order not found', async () => { const error = new Error('Order not found'); mockEngagementOrdersService.findById.mockRejectedValue(error);
await expect(controller.findOne('engagement-id', 'invalid-id')).rejects.toThrow( 'Order not found', ); }); });});Service Unit Test Template
Section titled βService Unit Test TemplateβServices contain business logic and should be tested in isolation. We mock the EntityManager or Repository to avoid database side effects.
Standard Pattern:
import { Test, TestingModule } from '@nestjs/testing';
import { EntityManager } from 'typeorm';
import { createTestingModule } from '@lib/common/testing';
import { createMockEngagementOrderDTO } from '@apps/data-owner-bc/test/mocks/mock-engagement-orders';
import { EngagementOrdersService } from '../../src/modules/engagement/services/engagement-orders.service';
describe('EngagementOrdersService (Unit)', () => { let service: EngagementOrdersService; let entityManager: EntityManager;
// 1. Define Mock EntityManager const mockEntityManager = { save: jest.fn(), findOne: jest.fn(), // ... add other necessary methods };
beforeEach(async () => { const module: TestingModule = await createTestingModule( [EngagementOrdersService], [ { provide: EntityManager, useValue: mockEntityManager, }, ], ).compile();
service = module.get<EngagementOrdersService>(EngagementOrdersService); entityManager = module.get<EntityManager>(EntityManager); });
afterEach(() => { jest.clearAllMocks(); // Critical: Clear mocks state });
describe('createEngagementOrder', () => { it('should successfully create and return an engagement order', async () => { // ARRANGE const engagementId = 'engagement-123'; const dto = createMockEngagementOrderDTO(engagementId); const expectedResult = { id: 'order-123', ...dto };
mockEntityManager.save.mockResolvedValue(expectedResult);
// ACT const result = await service.createEngagementOrder(engagementId, dto, {} as any);
// ASSERT expect(result).toEqual(expectedResult); expect(entityManager.save).toHaveBeenCalledTimes(1); }); });});E2E Testing Deep Dive
Section titled βE2E Testing Deep DiveβWhat is E2E Testing?
Section titled βWhat is E2E Testing?βE2E testing tests the full application flow, simulating a real HTTP request. This is the only way to test your applicationβs βwiringβ:
- Routing: Does
POST /engagements/:id/ordersmap to the correct controller method? - Global Pipes: Does the
ValidationPipecorrectly catch invalid DTOs and return a 400? - Global Interceptors: Does the
TransformInterceptorcorrectly format the response into the JSON:API standard? - Global Filters: Does the
AllExceptionsFiltercorrectly catch and format errors? - Decorators: Do
@Param(),@Body(), and@CurrentUser()inject the correct data?
Note: We mock the Service layer (useValue: mockService) to avoid database calls, but we test everything else (the entire HTTP request/response pipeline).
E2E Test Structure
Section titled βE2E Test StructureβE2E tests use Supertest to make real HTTP requests to a test application instance.
// Basic E2E test structuredescribe('POST /endpoint', () => { it('should create resource successfully', async () => { // 1. ARRANGE - Prepare mock data and setup mocks const dto = { /* valid data */ }; mockService.method.mockResolvedValue(mockEntity);
// 2. ACT - Make HTTP request using supertest const response = await request(server).post('/endpoint').send(dto).expect(201);
// 3. ASSERT - Verify service was called and response is correct expect(service.method).toHaveBeenCalledWith(dto); expect(response.body.data).toBeDefined(); });});E2E Test Template
Section titled βE2E Test TemplateβThis template uses our monorepoβs shared createTestingModule and createTestApp helpers, which automatically include global pipes and interceptors.
import * as http from 'http';import { INestApplication } from '@nestjs/common';
import request from 'supertest'; // 1. Import supertest
import { createTestApp, createTestingModule } from '@lib/common/testing'; // 2. Import shared helpers
import { createMockEngagementOrderDTO, createMockEngagementOrdersService, MockEngagementOrdersService,} from '@apps/data-owner-bc/test/mocks/mock-engagement-orders';
// 3. Import mock factoriesimport { EngagementOrdersController } from '../../src/modules/engagement/controllers/engagement-orders.controller';import { EngagementOrdersService } from '../../src/modules/engagement/services/engagement-orders.service';
/** * E2E Tests for EngagementOrders Controller * * These tests verify the full HTTP request/response pipeline including: * - Routing * - ValidationPipe (DTO validation) * - TransformInterceptor (JSON:API formatting) * - AllExceptionsFilter (error handling) */
describe('EngagementOrdersController (e2e)', () => { let app: INestApplication; let service: MockEngagementOrdersService; let server: http.Server;
const mockEngagementOrdersService = createMockEngagementOrdersService(); const resourceType = 'engagement-orders'; // For JSON:API assertions const MOCK_ENGAGEMENT_ID = '550e8400-e29b-41d4-a716-446655440000'; const MOCK_ORDER_ID = '550e8400-e29b-41d4-a716-446655440002';
beforeAll(async () => { // 4. Create the sandboxed module const moduleFixture = await createTestingModule( [EngagementOrdersController], [ { provide: EngagementOrdersService, useValue: mockEngagementOrdersService, }, ], );
// 5. Create the full Nest app (this runs .init() and registers all global pipes/interceptors) app = await createTestApp(moduleFixture);
// 6. Get the server instance for supertest service = moduleFixture.get(EngagementOrdersService) as MockEngagementOrdersService; server = app.getHttpServer() as http.Server; });
afterAll(async () => { await app.close(); });
beforeEach(() => { jest.clearAllMocks(); });
describe('POST /engagements/:engagementId/orders', () => { it('should create a new order and return 201 with JSON:API structure', async () => { // ARRANGE const createDTO = createMockEngagementOrderDTO(MOCK_ENGAGEMENT_ID);
// Mock service is already set up to return a mock entity
// ACT const response: request.Response = await request(server) .post(`/engagements/${MOCK_ENGAGEMENT_ID}/orders`) .send(createDTO) .expect(201); // 7. Assert HTTP status
// ASSERT // 8. Assert service was called correctly by the "real" controller expect(service.createEngagementOrder).toHaveBeenCalledWith( MOCK_ENGAGEMENT_ID, expect.any(Object), expect.any(Object), // currentUser );
// 9. Assert the "real" interceptor formatted the response correctly // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment expect(response.body).toEqual( expect.objectContaining({ data: expect.objectContaining({ type: resourceType, id: expect.any(String), attributes: expect.objectContaining({ engagement_id: MOCK_ENGAGEMENT_ID, }), }), status: expect.objectContaining({ code: 201000 }), links: expect.objectContaining({ self: expect.any(String) }), }), ); });
it('should return 400 for missing required fields (ValidationPipe test)', async () => { // ARRANGE const invalidDTO = createMockEngagementOrderDTO(MOCK_ENGAGEMENT_ID, { order_type: undefined, // This is required });
// ACT await request(server) .post(`/engagements/${MOCK_ENGAGEMENT_ID}/orders`) .send(invalidDTO) .expect(400); // 10. Assert the "real" ValidationPipe worked
// ASSERT expect(service.createEngagementOrder).not.toHaveBeenCalled(); });
it('should return 400 for invalid data types', async () => { const invalidDTO = { order_type: 12345, // Should be string order_code: 'LAB001', };
await request(server) .post(`/engagements/${MOCK_ENGAGEMENT_ID}/orders`) .send(invalidDTO) .expect(400);
expect(service.createEngagementOrder).not.toHaveBeenCalled(); });
it('should strip unknown properties (whitelist)', async () => { const dtoWithExtra = createMockEngagementOrderDTO(MOCK_ENGAGEMENT_ID, { unknownField: 'should be removed', // Should be stripped });
await request(server) .post(`/engagements/${MOCK_ENGAGEMENT_ID}/orders`) .send(dtoWithExtra) .expect(201);
expect(service.createEngagementOrder).toHaveBeenCalledWith( MOCK_ENGAGEMENT_ID, expect.not.objectContaining({ unknownField: 'should be removed' }), expect.any(Object), ); }); });
describe('PATCH /engagements/:engagementId/orders/:orderId', () => { it('should update an existing order', async () => { const updateDTO = { status: 'COMPLETED', };
await request(server) .patch(`/engagements/${MOCK_ENGAGEMENT_ID}/orders/${MOCK_ORDER_ID}`) .send(updateDTO) .expect(200);
expect(service.update).toHaveBeenCalledWith( MOCK_ORDER_ID, expect.objectContaining(updateDTO), expect.any(Object), ); });
it('should handle service errors gracefully', async () => { mockEngagementOrdersService.update.mockRejectedValueOnce(new Error('Order not found'));
await request(server) .patch(`/engagements/${MOCK_ENGAGEMENT_ID}/orders/invalid-id`) .send({ status: 'COMPLETED' }) .expect(500); }); });
describe('GET /engagements/:engagementId/orders', () => { it('should return paginated list with JSON:API structure', async () => { // ARRANGE // Mock service is already set up to return paginated data
// ACT const response: request.Response = await request(server) .get(`/engagements/${MOCK_ENGAGEMENT_ID}/orders`) .query({ page: 1, limit: 10 }) .expect(200);
// ASSERT expect(service.findPaginated).toHaveBeenCalled();
// 11. Assert the "real" interceptor formatted the paginated response // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment expect(response.body).toEqual( expect.objectContaining({ data: expect.any(Array), meta: expect.objectContaining({ pagination: expect.objectContaining({ page: 1, page_size: 10, }), }), links: expect.objectContaining({ self: expect.stringContaining('page=1&limit=10'), }), }), ); });
it('should use default pagination when no query params', async () => { await request(server).get(`/engagements/${MOCK_ENGAGEMENT_ID}/orders`).expect(200);
expect(service.findPaginated).toHaveBeenCalled(); }); });
describe('GET /engagements/:engagementId/orders/:orderId', () => { it('should return a single order by ID', async () => { const response: request.Response = await request(server) .get(`/engagements/${MOCK_ENGAGEMENT_ID}/orders/${MOCK_ORDER_ID}`) .expect(200);
expect(service.findById).toHaveBeenCalledWith(MOCK_ORDER_ID); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment expect(response.body).toEqual( expect.objectContaining({ data: expect.objectContaining({ type: resourceType, id: MOCK_ORDER_ID, }), }), ); });
it('should return 404 when order not found', async () => { mockEngagementOrdersService.findById.mockRejectedValueOnce(new Error('Order not found'));
await request(server).get(`/engagements/${MOCK_ENGAGEMENT_ID}/orders/invalid-id`).expect(404); }); });
describe('DELETE /engagements/:engagementId/orders/:orderId', () => { it('should soft delete an order', async () => { await request(server) .delete(`/engagements/${MOCK_ENGAGEMENT_ID}/orders/${MOCK_ORDER_ID}`) .expect(204);
expect(service.delete).toHaveBeenCalledWith( MOCK_ORDER_ID, true, // soft delete expect.any(Object), ); }); });});Continue Reading
Section titled βContinue Readingβ- Mocking & Configuration: Mock factories, Jest configuration (unit & E2E), best practices, and troubleshooting
- Coverage & Quick Reference: Coverage reports, summary checklist, Supertest methods, and JSON:API assertions