Jest toHaveBeenNthCalledWith为何取变量当前值而非调用时的值?
问题
我正在开发一个使用@nestjs/axios、@nestjs/testing等插件的NestJS应用,编写了一个分页请求API并收集结果ID的syncRecordList函数:
async syncRecordList(limit: number = 1000) { const url = 'https://my.test.url/records'; const headers = { Authorization: 'Bearer token-value', }; const params = { limit, // 每页条数 offset: 0, // 分页偏移量 }; const itemIds: number[] = []; let more: boolean = true; while (more) { const response = await this.httpService.axiosRef.get(url, { headers, params, }); response.data.items.forEach((item) => { itemIds.push(item.id); }); params.offset += limit; more = response.data.hasMore; } return itemIds; }
为测试该函数,我编写了如下测试用例:
describe('syncRecordList', () => { it('should call multiple times if there is pagination', async () => { const response0 = { data: { items: [{ id: 1 }, { id: 2 }], hasMore: true, }, }; const response1 = { data: { items: [{ id: 3 }, { id: 4 }], hasMore: false, }, }; const callExpectWith0 = [ 'https://my.test.url/records', { headers: { Authorization: 'Bearer token-value', }, params: { limit: 2, offset: 0 }, }, ] const callExpectWith1 = [ 'https://my.test.url/records', { headers: { Authorization: 'Bearer token-value', }, params: { limit: 2, offset: 2 }, }, ] const httpSpy = jest.spyOn(service.httpService.axiosRef, 'get') .mockResolvedValueOnce(response0) .mockResolvedValueOnce(response1); await service.syncRecordList(2); expect(httpSpy).toHaveBeenCalledTimes(2); expect(httpSpy).toHaveBeenNthCalledWith(1, ...callExpectWith0); expect(httpSpy).toHaveBeenNthCalledWith(2, ...callExpectWith1); }); });
我期望第一次调用使用limit: 2, offset: 0,第二次使用limit: 2, offset: 2,但测试失败,断言显示两次调用的offset均为4——即测试结束时params变量的最终值,而非调用时的实际值。
修改函数,每次调用时传入params的副本后测试即可通过:
async syncRecordList(type: NetSuiteRecordTypes, integrationId: string, limit: number = 1000) { // ... 省略其他代码 const params = { limit, // 每页条数 offset: 0, // 分页偏移量 }; const itemIds: number[] = []; let more: boolean = true; while (more) { // 为让测试通过添加的逻辑:params对象被修改导致测试失败,因Jest默认引用变量当前值而非调用时的实际值 const paramsToUse = { ...params }; const response = await this.httpService.axiosRef.get(url, { headers, params: paramsToUse, }); response.data.items.forEach((item) => { itemIds.push(item.id); }); params.offset += limit; more = response.data.hasMore; } return itemIds; }
我的问题是:有没有办法让Jest的toHaveBeenNthCalledWith断言使用调用时传入的参数值,而非测试结束时变量的当前值?
解决方案
方法1:手动捕获调用时的参数快照
Jest默认保存参数的引用而非值的副本,可在mock函数内手动深拷贝并记录每次调用的参数:
it('should call multiple times if there is pagination', async () => { const response0 = { data: { items: [{ id: 1 }, { id: 2 }], hasMore: true } }; const response1 = { data: { items: [{ id: 3 }, { id: 4 }], hasMore: false } }; const callExpectWith0 = ['https://my.test.url/records', { headers: { Authorization: 'Bearer token-value' }, params: { limit: 2, offset: 0 } }]; const callExpectWith1 = ['https://my.test.url/records', { headers: { Authorization: 'Bearer token-value' }, params: { limit: 2, offset: 2 } }]; const callArgs = []; const httpSpy = jest.spyOn(service.httpService.axiosRef, 'get') .mockImplementation((url, config) => { // 深拷贝参数,保存调用时的快照 callArgs.push([url, JSON.parse(JSON.stringify(config))]); return callArgs.length === 1 ? Promise.resolve(response0) : Promise.resolve(response1); }); await service.syncRecordList(2); expect(httpSpy).toHaveBeenCalledTimes(2); expect(callArgs[0]).toEqual(callExpectWith0); expect(callArgs[1]).toEqual(callExpectWith1); });
通过JSON.parse(JSON.stringify(config))深拷贝参数,确保记录的是调用时的参数状态,而非后续被修改的引用。
方法2:自定义mock函数的参数捕获逻辑
使用jest.fn()创建mock,手动保存参数的副本到mock.calls:
it('should call multiple times if there is pagination', async () => { const response0 = { data: { items: [{ id: 1 }, { id: 2 }], hasMore: true } }; const response1 = { data: { items: [{ id: 3 }, { id: 4 }], hasMore: false } }; const callExpectWith0 = ['https://my.test.url/records', { headers: { Authorization: 'Bearer token-value' }, params: { limit: 2, offset: 0 } }]; const callExpectWith1 = ['https://my.test.url/records', { headers: { Authorization: 'Bearer token-value' }, params: { limit: 2, offset: 2 } }]; const mockGet = jest.fn((url, config) => { // 手动添加深拷贝后的参数到mock调用记录 mockGet.mock.calls.push([url, { ...config, params: { ...config.params } }]); return mockGet.mock.calls.length === 1 ? Promise.resolve(response0) : Promise.resolve(response1); }); service.httpService.axiosRef.get = mockGet; await service.syncRecordList(2); expect(mockGet).toHaveBeenCalledTimes(2); expect(mockGet.mock.calls[0]).toEqual(callExpectWith0); expect(mockGet.mock.calls[1]).toEqual(callExpectWith1); });
这种方式绕过Jest默认的引用保存逻辑,直接存储调用时的参数副本。
方法3:保持原函数的参数副本传递(推荐)
你当前修改函数,每次创建params副本再传入的方式不仅能解决测试问题,还让函数逻辑更清晰——传递参数快照而非引用,避免后续修改影响已发出的请求(即使axios内部会处理参数,显式创建副本也更符合代码可读性要求)。这种方式无需修改测试代码,同时优化了业务逻辑的健壮性。
内容的提问来源于stack exchange,提问作者undertakingyou
相关产品推荐
相关产品推荐

