NestJS Testing Overview
NestJS provides comprehensive testing support, including unit testing, integration testing, and end-to-end testing. The testing framework is based on Jest and provides rich testing tools and utilities.
Testing Types
1. Unit Testing
Unit tests are used to test the behavior of a single function, class, or module, typically without involving external dependencies.
Service Unit Test Example
typescriptimport { Test, TestingModule } from '@nestjs/testing'; import { UsersService } from './users.service'; import { getRepositoryToken } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; import { Repository } from 'typeorm'; describe('UsersService', () => { let service: UsersService; let repository: Repository<User>; const mockUserRepository = { create: jest.fn(), save: jest.fn(), find: jest.fn(), findOne: jest.fn(), update: jest.fn(), delete: jest.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, { provide: getRepositoryToken(User), useValue: mockUserRepository, }, ], }).compile(); service = module.get<UsersService>(UsersService); repository = module.get<Repository<User>>(getRepositoryToken(User)); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('create', () => { it('should create a new user', async () => { const createUserDto = { name: 'John', email: 'john@example.com' }; const user = { id: 1, ...createUserDto }; mockUserRepository.create.mockReturnValue(user); mockUserRepository.save.mockResolvedValue(user); const result = await service.create(createUserDto); expect(repository.create).toHaveBeenCalledWith(createUserDto); expect(repository.save).toHaveBeenCalledWith(user); expect(result).toEqual(user); }); }); describe('findAll', () => { it('should return an array of users', async () => { const users = [ { id: 1, name: 'John', email: 'john@example.com' }, { id: 2, name: 'Jane', email: 'jane@example.com' }, ]; mockUserRepository.find.mockResolvedValue(users); const result = await service.findAll(); expect(repository.find).toHaveBeenCalled(); expect(result).toEqual(users); }); }); });
2. Integration Testing
Integration tests are used to test the interaction between multiple components, typically involving databases, external services, etc.
Controller Integration Test Example
typescriptimport { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; beforeEach(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); }); it('/users (GET)', () => { return request(app.getHttpServer()) .get('/users') .expect(200) .expect([]); }); it('/users (POST)', () => { return request(app.getHttpServer()) .post('/users') .send({ name: 'John', email: 'john@example.com' }) .expect(201) .expect(res => { expect(res.body).toHaveProperty('id'); expect(res.body.name).toBe('John'); }); }); afterEach(async () => { await app.close(); }); });
3. End-to-End Testing
E2E tests simulate real user scenarios, testing the entire application flow.
typescriptimport { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; describe('UsersController (e2e)', () => { let app: INestApplication; let userId: number; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe()); await app.init(); }); it('should create a user', async () => { const response = await request(app.getHttpServer()) .post('/users') .send({ name: 'John Doe', email: 'john@example.com' }) .expect(201); userId = response.body.id; expect(response.body).toHaveProperty('id'); expect(response.body.name).toBe('John Doe'); }); it('should get all users', async () => { const response = await request(app.getHttpServer()) .get('/users') .expect(200); expect(Array.isArray(response.body)).toBe(true); }); it('should get a user by id', async () => { const response = await request(app.getHttpServer()) .get(`/users/${userId}`) .expect(200); expect(response.body.id).toBe(userId); }); it('should update a user', async () => { const response = await request(app.getHttpServer()) .patch(`/users/${userId}`) .send({ name: 'Jane Doe' }) .expect(200); expect(response.body.name).toBe('Jane Doe'); }); it('should delete a user', async () => { await request(app.getHttpServer()) .delete(`/users/${userId}`) .expect(200); }); afterAll(async () => { await app.close(); }); });
Testing Tools and Utilities
1. Test.createTestingModule()
Create a test module for configuring the test environment.
typescriptconst module: TestingModule = await Test.createTestingModule({ imports: [AppModule], providers: [UsersService], }).compile();
2. Mock Providers
Use mock objects to replace real providers.
typescript{ provide: UsersService, useValue: { findAll: jest.fn().mockResolvedValue([]), findOne: jest.fn().mockResolvedValue({ id: 1 }), }, }
3. Jest Functions
Use Jest's mock functionality.
typescriptjest.fn() // Create mock function jest.mock() // Module mock jest.spyOn() // Spy on object methods mockResolvedValue() // Set return value mockRejectedValue() // Set rejection value mockReturnValue() // Set synchronous return value mockReturnValueOnce() // Set one-time return value
Best Practices
1. Test Organization
shellsrc/ ├── users/ │ ├── users.controller.spec.ts │ ├── users.service.spec.ts │ └── users.module.spec.ts test/ ├── app.e2e-spec.ts └── users.e2e-spec.ts
2. Test Naming Conventions
- Use
describeto group related tests - Use
itortestto describe individual test cases - Test names should clearly describe what is being tested
typescriptdescribe('UsersService', () => { describe('create', () => { it('should create a new user', async () => { // Test logic }); it('should throw an error if email already exists', async () => { // Test logic }); }); });
3. Test Isolation
Each test should run independently without relying on state from other tests.
typescriptbeforeEach(() => { // Reset state before each test }); afterEach(() => { // Clean up after each test });
4. Test Coverage
Use Jest's coverage feature to ensure code quality.
bashnpm run test:cov
Configure in package.json:
json{ "jest": { "collectCoverageFrom": [ "src/**/*.(t|j)s", "!src/main.ts", "!src/**/*.module.ts", "!src/**/*.dto.ts" ], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } } }
5. Mock External Dependencies
For external dependencies like databases, APIs, use mocks to avoid actual calls.
typescriptconst mockRepository = { find: jest.fn(), findOne: jest.fn(), save: jest.fn(), delete: jest.fn(), };
6. Test Asynchronous Code
Properly handle testing of asynchronous code.
typescript// Use async/await it('should return user', async () => { const result = await service.findOne(1); expect(result).toBeDefined(); }); // Use Promise it('should return user', () => { return service.findOne(1).then(result => { expect(result).toBeDefined(); }); }); // Use done callback it('should return user', (done) => { service.findOne(1).then(result => { expect(result).toBeDefined(); done(); }); });
7. Test Edge Cases
Ensure tests cover various edge cases and error scenarios.
typescriptdescribe('findOne', () => { it('should return user when found', async () => { // Normal case }); it('should throw NotFoundException when user not found', async () => { // User not found case }); it('should throw BadRequestException when id is invalid', async () => { // Invalid ID case }); });
8. Use Test Database
Use a test database in integration tests to avoid affecting production data.
typescriptbeforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot({ type: 'sqlite', database: ':memory:', entities: [User], synchronize: true, }), ], }).compile(); });
Performance Testing
1. Load Testing
Use tools like Artillery or k6 for load testing.
javascript// artillery.config.js module.exports = { config: { target: 'http://localhost:3000', phases: [ { duration: 60, arrivalRate: 10 }, ], }, scenarios: [ { flow: [ { get: { url: '/users' } }, ], }, ], };
2. Response Time Testing
Ensure API response times are within acceptable ranges.
typescriptit('should respond within 200ms', async () => { const start = Date.now(); await request(app.getHttpServer()).get('/users'); const duration = Date.now() - start; expect(duration).toBeLessThan(200); });
Code Quality Best Practices
1. Code Style
Use ESLint and Prettier to maintain consistent code style.
bashnpm run lint npm run format
2. Type Safety
Fully utilize TypeScript's type system.
typescriptinterface CreateUserDto { name: string; email: string; }
3. Error Handling
Unified error handling mechanism.
typescript@Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { // Unified error handling logic } }
4. Logging
Use NestJS Logger for structured logging.
typescriptimport { Logger } from '@nestjs/common'; export class UsersService { private readonly logger = new Logger(UsersService.name); async findAll() { this.logger.log('Finding all users'); // Business logic } }
5. Environment Configuration
Use @nestjs/config to manage environment variables.
typescriptimport { ConfigService } from '@nestjs/config'; export class UsersService { constructor(private configService: ConfigService) { const apiKey = this.configService.get('API_KEY'); } }
6. Documentation
Use Swagger to generate API documentation.
typescriptimport { ApiTags, ApiOperation } from '@nestjs/swagger'; @ApiTags('users') @Controller('users') export class UsersController { @Get() @ApiOperation({ summary: 'Get all users' }) findAll() { return this.usersService.findAll(); } }
Summary
NestJS testing and best practices provide:
- Comprehensive testing support framework
- Flexible testing tools and utilities
- Clear test organization structure
- High-quality code standards
- Excellent development experience
Mastering testing and best practices is key to building high-quality NestJS applications. Through comprehensive test coverage and following best practices, you can ensure the reliability, maintainability, and scalability of your applications. Testing is not just quality assurance, but an important part of the development process.