Nest.js单元测试Mock ClickHouse时出现Cannot read properties of undefined (reading 'toPromise')错误排查
问题分析与解决方案
你遇到的这个错误,核心原因有两个:
1. 异步测试未正确等待,导致Mock被提前清理
第二个测试用例中,你使用了.then().catch()来处理createAccount的异步操作,但没有返回这个Promise,也没有用await等待它完成。Jest会认为测试已经执行完毕,提前清理所有Mock。当后续createAccount的异步代码继续执行时,getBySlug的Mock已经失效,转而执行真实逻辑——调用this.db.query(),而你Mock的query方法默认返回undefined,自然就会抛出Cannot read properties of undefined (reading 'toPromise')的错误。
2. 测试传参错误,不符合业务逻辑
你的createAccount方法期望接收一个包含slug和name的对象参数,但测试中却传入了fakeAccount.name(一个字符串)。这会导致args.slug为undefined,虽然Mock能暂时处理,但不符合实际业务场景,也可能引发其他潜在问题。
修正后的测试代码
我帮你调整了测试用例,解决了上述问题:
import { Test, TestingModule } from '@nestjs/testing'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Account } from '../accounts/entities/account.entity'; import { AccountsService } from '../accounts/accounts.service'; const now = new Date(); const fakeAccount: Account = { id: 'lalal', slug: 'palmeiras', name: 'Palmeiras', createdAt: now, }; jest.mock('@depyronick/nestjs-clickhouse'); jest.mock('newrelic', () => ({ recordMetric: jest.fn(), recordCustomEvent: jest.fn(), })); describe('AccountsService', () => { let service: AccountsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AccountsService, { provide: 'database', useValue: { // 提前给insert/query返回带有toPromise的对象,避免真实调用报错 insert: jest.fn().mockReturnValue({ toPromise: jest.fn().mockResolvedValue(undefined) }), query: jest.fn().mockReturnValue({ toPromise: jest.fn().mockResolvedValue(null) }), }, }, ], }).compile(); service = module.get<AccountsService>(AccountsService); }); it('should be defined', () => { expect(service).toBeDefined(); }); describe('creating an account', () => { it('should create account if not exists', async () => { const createArgs = { slug: fakeAccount.slug, name: fakeAccount.name }; const insertSpy = jest.spyOn(service['db'], 'insert'); const event = await service.createAccount(createArgs); expect(service.getBySlug).toHaveBeenCalledWith(createArgs.slug); expect(insertSpy).toHaveBeenCalledWith('accounts', expect.arrayContaining([ expect.objectContaining({ slug: createArgs.slug, name: createArgs.name, }), ])); expect(event.name).toEqual(createArgs.name); expect(event.createdAt).toEqual(now); }); it('should return error if account exists', async () => { const createArgs = { slug: fakeAccount.slug, name: fakeAccount.name }; // 覆盖query的返回值,模拟账号已存在 jest.spyOn(service['db'], 'query').mockReturnValue({ toPromise: jest.fn().mockResolvedValue(fakeAccount), }); const insertSpy = jest.spyOn(service['db'], 'insert'); // 使用expect.rejects来断言异步错误,Jest会自动等待Promise完成 await expect(service.createAccount(createArgs)).rejects.toThrow( new HttpException('Account name already exists', HttpStatus.BAD_REQUEST) ); expect(service.getBySlug).toHaveBeenCalledWith(createArgs.slug); expect(insertSpy).not.toHaveBeenCalled(); }); }); });
关键修改点说明
- 正确处理异步测试:使用
await expect(...).rejects.toThrow()来断言异步错误,Jest会自动等待Promise完成,避免Mock被提前清理。 - 修复传参问题:给
createAccount传入包含slug和name的对象,符合业务逻辑。 - 优化Mock方式:在
beforeEach中提前给insert和query方法返回带有toPromise的Mock对象,避免真实调用时因返回undefined报错;测试时按需覆盖返回值即可。
内容的提问来源于stack exchange,提问作者Luis Novo
相关产品推荐
相关产品推荐

