NestJS Service调用Repository的Mock测试失败,求排查解决
问题分析与修复
问题场景
我有一个NestJS的TestService,其中getAllTests方法通过Repository的QueryBuilder查询指定账户的TestEntity数据。为该方法编写了单元测试,但两个测试用例均失败,以下是相关代码及报错信息:
Service代码
@Injectable() export class TestService { constructor( @InjectRepository(TestEntity) private readonly repo: Repository<TestEntity>, ) {} async getAllTests(acct: AccountEntity): Promise<Array<TestEntity>> { try { const query = this.repo.createQueryBuilder(DB_TABLE_NAME_TEST); query.where({ account: acct.id }); return await query.getMany(); } catch (ex) { throw new InternalServerErrorException(); } } }
原测试代码
const mockAccount: AccountEntity = { id: 'mock-account-id-1234', email: 'test@testerson.org', }; const mockTest: TestEntity = { id: '1234-5678-9012', account: 'mock-account-id-1234', description: 'Test Name', testerName: 'Bob Jones', testerEmail: 'bob@aol.com', }; describe('TestService', () => { let service: TestService; let repo: Repository<TestEntity>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ TestService, { provide: getRepositoryToken(TestEntity), useValue: { create: jest.fn(), save: jest.fn(), delete: jest.fn(), createQueryBuilder: jest.fn(() => ({ where: jest.fn(), getMany: jest.fn(), getOne: jest.fn(), })), }, }, ], }).compile(); service = module.get(TestService); repo = module.get(getRepositoryToken(TestEntity)); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('getAllTests()', () => { it('should successfully get a list of all tests for the provided account', async () => { jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'where') jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockResolvedValue([mockTest]); expect(repo.createQueryBuilder(DB_TABLE_NAME_TEST).where).toHaveBeenCalledWith({ account: mockAccount.id }); await expect(service.getAllTests(mockAccount)).resolves.toEqual([mockTest]); }); it('should throw an error when the query fails', async () => { jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'where').mockImplementation(() => { throw new Error(); }); jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockImplementation(() => { throw new Error(); }); const result = () => service.getAllTests(mockAccount); await expect(result).rejects.toThrow(InternalServerErrorException); }); }); });
原报错信息
FAIL src/test/test.service.spec.ts ● TestService › getAllTests() › should successfully get a list of all tests for the provided account expect(jest.fn()).toHaveBeenCalledWith(...expected) Expected: {"account": "mock-account-id-1234"} Number of calls: 0 143 | jest.spyOn(repo.createQueryBuilder(DB_TABLE_NAME_TEST), 'getMany').mockResolvedValue([mockTest]); 144 | > 145 | expect(repo.createQueryBuilder(DB_TABLE_NAME_TEST).where).toHaveBeenCalledWith({ account: mockAccount.id }); | ^ 146 | await expect(service.getAllTests(mockAccount)).resolves.toEqual([mockTest]); 147 | }); 148 | at Object.<anonymous> (test/test.service.spec.ts:145:65) ● TestService › getAllTests() › should throw an error when the query fails expect(received).rejects.toThrow() Received promise resolved instead of rejected Resolved to value: undefined 157 | 158 | const result = () => service.getAllTests(mockAccount); > 159 | await expect(result).rejects.toThrow(InternalServerErrorException); | ^ 160 | }); 161 | }); 162 | }); at expect (../node_modules/expect/build/index.js:113:15) at Object.<anonymous> (test/test.service.spec.ts:159:13)
错误原因
- QueryBuilder实例不统一:原测试中每次调用
repo.createQueryBuilder()都会生成新的mock对象,导致测试代码中spy的实例和Service实际调用的实例不是同一个,因此断言where被调用时显示调用次数为0。 - 断言顺序错误:第一个测试中先断言
where的调用,再执行Service方法,此时方法还未执行,自然没有调用记录。 - 链式调用未正确模拟:原QueryBuilder的mock中
where方法没有返回this,不符合NestJS QueryBuilder的实际行为,可能导致调用链异常。 - 第二个测试的错误模拟未生效:同样因为实例不统一,模拟的
getMany抛出错误没有被Service捕获,导致Promise没有被拒绝,反而返回undefined。
修复后的测试代码
const mockAccount: AccountEntity = { id: 'mock-account-id-1234', email: 'test@testerson.org', }; const mockTest: TestEntity = { id: '1234-5678-9012', account: 'mock-account-id-1234', description: 'Test Name', testerName: 'Bob Jones', testerEmail: 'bob@aol.com', }; describe('TestService', () => { let service: TestService; let repo: Repository<TestEntity>; let queryBuilderMock: any; beforeEach(async () => { // 提前定义QueryBuilder的mock实例,确保每次createQueryBuilder返回同一个对象 queryBuilderMock = { where: jest.fn().mockReturnThis(), getMany: jest.fn(), getOne: jest.fn(), }; const module: TestingModule = await Test.createTestingModule({ providers: [ TestService, { provide: getRepositoryToken(TestEntity), useValue: { create: jest.fn(), save: jest.fn(), delete: jest.fn(), createQueryBuilder: jest.fn(() => queryBuilderMock), }, }, ], }).compile(); service = module.get(TestService); repo = module.get(getRepositoryToken(TestEntity)); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('getAllTests()', () => { it('should successfully get a list of all tests for the provided account', async () => { // 模拟getMany的返回值 queryBuilderMock.getMany.mockResolvedValue([mockTest]); // 先执行Service方法 const result = await service.getAllTests(mockAccount); // 再断言调用情况和返回值 expect(repo.createQueryBuilder).toHaveBeenCalledWith(DB_TABLE_NAME_TEST); expect(queryBuilderMock.where).toHaveBeenCalledWith({ account: mockAccount.id }); expect(result).toEqual([mockTest]); }); it('should throw an error when the query fails', async () => { // 模拟getMany抛出错误 queryBuilderMock.getMany.mockRejectedValue(new Error('Query failed')); // 断言抛出指定异常 await expect(service.getAllTests(mockAccount)).rejects.toThrow(InternalServerErrorException); }); }); });
修复要点
- 提前定义QueryBuilder的mock实例,确保
createQueryBuilder每次返回同一个对象,保证测试和Service使用的是同一个实例。 - 调整断言顺序:先执行Service方法,再断言方法的调用情况和返回值。
- 给
where方法添加mockReturnThis(),符合QueryBuilder链式调用的实际行为。 - 第二个测试中直接模拟
getMany抛出错误,确保Service的try/catch能捕获并转换为InternalServerErrorException。
内容的提问来源于stack exchange,提问作者Chris Barr
相关产品推荐
相关产品推荐

