1.NestJS測試
NestJS 内置并提供了 Jest @nestjs/測試 測試模組的包。
src/
├── users/
│ ├── users.service.ts
│ ├── users.service.spec.ts ← Unit test
│ ├── users.controller.ts
│ ├── users.controller.spec.ts ← Unit test
│ └── users.e2e-spec.ts ← E2E test
└── test/
└── app.e2e-spec.ts ← Global E2E
2. 單元測試服務
// users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
describe('UsersService', () => {
let service: UsersService;
let repo: jest.Mocked<Repository<User>>;
const mockRepo = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
delete: jest.fn(),
create: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepo },
],
}).compile();
service = module.get(UsersService);
repo = module.get(getRepositoryToken(User));
});
afterEach(() => jest.clearAllMocks());
describe('findAll', () => {
it('should return array of users', async () => {
const users = [{ id: '1', name: 'Test', email: '[email protected]' }];
mockRepo.find.mockResolvedValue(users);
const result = await service.findAll();
expect(result).toEqual(users);
expect(mockRepo.find).toHaveBeenCalledTimes(1);
});
});
describe('findById', () => {
it('should return user when found', async () => {
const user = { id: '1', name: 'Test' };
mockRepo.findOne.mockResolvedValue(user);
const result = await service.findById('1');
expect(result).toEqual(user);
expect(mockRepo.findOne).toHaveBeenCalledWith({ where: { id: '1' } });
});
it('should throw NotFoundException when not found', async () => {
mockRepo.findOne.mockResolvedValue(null);
await expect(service.findById('999')).rejects.toThrow(NotFoundException);
});
});
describe('create', () => {
it('should create and return a user', async () => {
const dto = { name: 'New User', email: '[email protected]', password: 'pass123' };
const saved = { id: '2', ...dto };
mockRepo.create.mockReturnValue(saved);
mockRepo.save.mockResolvedValue(saved);
const result = await service.create(dto);
expect(result).toEqual(saved);
});
});
});
3. 單元測試控制器
// users/users.controller.spec.ts
describe('UsersController', () => {
let controller: UsersController;
let service: jest.Mocked<UsersService>;
const mockService = {
findAll: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [UsersController],
providers: [{ provide: UsersService, useValue: mockService }],
}).compile();
controller = module.get(UsersController);
service = module.get(UsersService);
});
it('GET /users should return all users', async () => {
const users = [{ id: '1', name: 'Test' }];
mockService.findAll.mockResolvedValue(users);
const result = await controller.findAll();
expect(result).toEqual(users);
});
it('POST /users should create user', async () => {
const dto = { name: 'New', email: '[email protected]', password: 'pass' };
const created = { id: '2', ...dto };
mockService.create.mockResolvedValue(created);
const result = await controller.create(dto);
expect(result).toEqual(created);
expect(mockService.create).toHaveBeenCalledWith(dto);
});
});
4. 测试防护装置和管道
// Testing custom guard
describe('RolesGuard', () => {
let guard: RolesGuard;
let reflector: Reflector;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [RolesGuard, Reflector],
}).compile();
guard = module.get(RolesGuard);
reflector = module.get(Reflector);
});
it('should allow access for correct role', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
const context = createMockExecutionContext({
user: { role: 'admin' },
});
expect(guard.canActivate(context)).toBe(true);
});
it('should deny access for wrong role', () => {
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(['admin']);
const context = createMockExecutionContext({
user: { role: 'user' },
});
expect(guard.canActivate(context)).toBe(false);
});
});
5. 端對端測試
// test/users.e2e-spec.ts
import * as request from 'supertest';
import { INestApplication } from '@nestjs/common';
describe('Users (e2e)', () => {
let app: INestApplication;
let accessToken: string;
beforeAll(async () => {
const moduleFixture = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(DataSource)
.useValue(testDataSource)
.compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe());
await app.init();
// Login để lấy token
const loginRes = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: '[email protected]', password: 'password' });
accessToken = loginRes.body.accessToken;
});
afterAll(async () => {
await app.close();
});
describe('GET /users', () => {
it('should return 401 without token', () => {
return request(app.getHttpServer())
.get('/users')
.expect(401);
});
it('should return users with valid token', () => {
return request(app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body.data)).toBe(true);
});
});
});
describe('POST /users', () => {
it('should create user with valid data', () => {
return request(app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${accessToken}`)
.send({ name: 'E2E User', email: '[email protected]', password: 'pass1234' })
.expect(201)
.expect((res) => {
expect(res.body.data).toHaveProperty('id');
expect(res.body.data.email).toBe('[email protected]');
});
});
it('should return 400 with invalid email', () => {
return request(app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${accessToken}`)
.send({ name: 'Bad', email: 'invalid', password: 'pass' })
.expect(400);
});
});
});
6. 使用测试容器进行数据库测试
// test/setup.ts
import { DataSource } from 'typeorm';
export const testDataSource = new DataSource({
type: 'sqlite',
database: ':memory:',
entities: [User, Post, Order],
synchronize: true,
});
beforeAll(async () => {
await testDataSource.initialize();
// Seed test data
await testDataSource.getRepository(User).save({
email: '[email protected]',
name: 'Admin',
password: await bcrypt.hash('password', 10),
role: 'admin',
});
});
afterAll(async () => {
await testDataSource.destroy();
});
七、總結
- 單元測試:模拟依赖,测试逻辑隔离
- 控制器測試:模拟服务,测试路由/验证
- 防護罩/管道測試:模拟执行上下文、反射器
- 端對端測試:超級測試,真實的HTTP請求,測試完整的管道
- 資料庫測試:内存中的 SQLite 或 TestContainers
下一篇文章將探討 适用于 NestJS 的 Dockerize 和 CI/CD。