NestJS拦截器单元测试:如何验证响应头设置是否正确
Nest拦截器单元测试响应头校验方案
业务场景
编写的拦截器会拦截GET请求,并根据特定条件向响应添加对应响应头。
控制器代码
@UseInterceptors(ServiceHeaderInterceptor) @Get('users') public async users( @Query('id') Id: number, @Query('name') Name: string, ){ const setFlags: MetaFlags = {setCacheHeader: false, setServiceHeader: false}; const data = await this.service.getUsers(id, name, setFlags); return data; }
拦截器代码
注意:原拦截器存在变量名笔误,获取响应对象时定义的变量名为req,后续调用setHeader时用的是res,运行会报未定义错误,需先修正为const res = context.switchToHttp().getResponse();
@Injectable() export class HeaderInterceptor implements NestInterceptor { public intercept( context: ExecutionContext, next: CallHandler, ): Observable<any> { return next.handle().pipe( map((data: { data: DataType, header: 'cache' | 'database' }) => { const res = context.switchToHttp().getResponse(); if (data.header === 'database') { res.setHeader('x-api-key', 'pretty secure'); } else { res.setHeader(xyz, value); } return data.data; }), ); } }
原有测试代码
const users = [ { 'id': 1, 'name': 'Adam', }, { 'id': 2, 'name': 'Evan', }, ] const executionContext: any = { switchToHttp: jest.fn().mockReturnThis(), getResponse: jest.fn().mockReturnThis(), setHeader: jest.fn().mockReturnThis(), }; const next = { handle: () => of(users), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { provide: UserService, useValue: mock<UserService>(), }, ServiceHeaderInterceptor, ], }).compile(); interceptor = module.get(ServiceHeaderInterceptor); }); it('get users',async () => { interceptor.intercept(executionContext, next).subscribe({ next: (value) => { expect(value).toEqual(users); }, error: (error) => { throw error; }, }); }); });
存在问题
当前测试仅能校验响应体内容,无法验证拦截器是否正确设置响应头,核心原因有两点:
next.handle()返回的数据流直接返回users数组,和拦截器map操作符预期的{data, header}结构不匹配,根本不会进入响应头设置的判断逻辑- 响应对象的
setHeader方法虽然做了mock,但没有单独保留mock实例,无法追踪调用传参
修正方案
- 单独创建可追踪的response mock对象,将
setHeader定义为jest mock函数,再注入到executionContext的mock逻辑中 - 针对不同的响应头场景,构造对应结构的
next.handle()返回值 - 在测试断言中,除了校验返回的响应体,额外断言
setHeader的调用次数、传入的头名称和头取值
修正后的测试代码示例
// 单独定义mock响应对象,保留setHeader的mock引用方便断言 let mockSetHeader: jest.Mock; let mockResponse: any; let executionContext: any; const testUsers = [ { id: 1, name: 'Adam' }, { id: 2, name: 'Evan' }, ]; beforeEach(async () => { // 每个用例执行前重置mock,避免用例间状态污染 mockSetHeader = jest.fn().mockReturnThis(); mockResponse = { setHeader: mockSetHeader }; executionContext = { switchToHttp: jest.fn().mockReturnThis(), getResponse: jest.fn().mockReturnValue(mockResponse), }; const module: TestingModule = await Test.createTestingModule({ providers: [ { provide: UserService, useValue: mock<UserService>(), }, ServiceHeaderInterceptor, ], }).compile(); interceptor = module.get(ServiceHeaderInterceptor); }); // 测试header为database的场景 it('should set x-api-key header when data source is database', (done) => { const testNext = { handle: () => of({ data: testUsers, header: 'database' }) }; interceptor.intercept(executionContext, testNext).subscribe({ next: (value) => { // 校验响应体正确 expect(value).toEqual(testUsers); // 校验setHeader被正确调用 expect(mockSetHeader).toHaveBeenCalledTimes(1); expect(mockSetHeader).toHaveBeenCalledWith('x-api-key', 'pretty secure'); done(); }, error: (error) => done(error), }); }); // 测试header为cache的场景(对应代码里的else分支,替换成实际的xyz和value值即可) it('should set cache header when data source is cache', (done) => { const testCacheValue = 'max-age=3600'; const testNext = { handle: () => of({ data: testUsers, header: 'cache' }) }; interceptor.intercept(executionContext, testNext).subscribe({ next: (value) => { expect(value).toEqual(testUsers); expect(mockSetHeader).toHaveBeenCalledTimes(1); expect(mockSetHeader).toHaveBeenCalledWith(xyz, testCacheValue); // 替换xyz为实际的响应头名称 done(); }, error: (error) => done(error), }); });
注意:测试Observable异步逻辑时,用done回调标记异步完成,避免用例提前结束导致断言漏跑
内容的提问来源于stack exchange,提问作者salt-pepper
相关产品推荐
相关产品推荐

