NestJS测试疑问:为何jest.fn()无法让Prisma findUnique返回null?
我正在测试一个简单的createUser行为,希望通过jest.fn()让Prisma的findUnique方法返回null,但未能成功。如果通过jest.spyOn设置mock返回值,测试就能通过。
当我移除jest.spyOn(prismaMock.user, 'findUnique').mockReturnValue(null);这行代码,直接让jest.fn()返回null时,就会触发ConflictException: User already exists,不符合预期。我是不是忽略了jest.fn()的某些特性?
user.service.spec.ts
// [..] omitted imports describe('UserService', () => { let service: UserService; const prismaMock = { user: { create: jest.fn(() => { return { id: 1, name: 'any_name', email: 'any_email@mail.com' }; }), findUnique: jest.fn(), }, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: PrismaService, useValue: prismaMock }, ], }).compile(); service = module.get<UserService>(UserService); }); // [...] omitted tests above it('should return a new user', async () => { const createUserInput = { name: 'any_name', email: 'any_email@mail.com', }; jest.spyOn(prismaMock.user, 'findUnique').mockReturnValue(null); const response = await service.create(createUserInput); expect(response).toEqual({ id: expect.any(Number), name: 'any_name', email: 'any_email@mail.com', }); });
user.service.ts
// omitted imports @Injectable() export class UserService { constructor(private readonly prisma: PrismaService) {} async create(createUserDto: CreateUserDto): Promise<User> { const userAlreadyExists = await this.prisma.user.findUnique({ where: { email: createUserDto.email }, }); if (userAlreadyExists) { throw new ConflictException('User already exists'); } const createdUser = this.prisma.user.create({ data: createUserDto }); return createdUser; } }
核心问题出在mock函数的默认行为和测试用例之间的mock污染上,具体原因和解决方法如下:
1. jest.fn()的默认返回值不是null
你当前定义的findUnique: jest.fn(),这个mock函数默认调用后返回的是undefined,而不是null。虽然await undefined得到的还是undefined,理论上不会触发if (userAlreadyExists)的判断,但你说移除spyOn后触发了冲突异常,这说明之前的测试用例修改了共享的prismaMock对象——因为prismaMock是在describe外部定义的,所有测试用例会复用这个对象,若之前的测试给findUnique设置过返回用户对象的mock,且没有重置,当前测试就会沿用这个返回值,导致触发“用户已存在”的异常。
2. 异步方法的mock需要返回Promise
Prisma的所有数据库操作都是异步的,返回Promise。即使你想让findUnique返回null,也不能直接写jest.fn(() => null),虽然await可以处理非Promise值,但更规范的做法是用jest.fn(() => Promise.resolve(null))来模拟异步行为,和Prisma的实际行为保持一致。
3. spyOn的作用是覆盖并重置mock
jest.spyOn(prismaMock.user, 'findUnique').mockReturnValue(null)会重新包装原有的mock函数,强制给当前测试用例设置新的返回值,相当于临时覆盖了之前可能被修改过的mock实现,所以测试能通过。
解决方法
方法一:初始化时设置正确的异步mock返回值
直接在prismaMock里给findUnique设置默认返回resolved为null的Promise:
const prismaMock = { user: { create: jest.fn(() => Promise.resolve({ id: 1, name: 'any_name', email: 'any_email@mail.com' })), // 模拟异步返回null findUnique: jest.fn(() => Promise.resolve(null)), }, };
注意:你的service里的create方法没有用await接收prisma.create的结果,实际返回的是Promise而非User对象,建议加上await:
const createdUser = await this.prisma.user.create({ data: createUserDto });
方法二:在beforeEach中重置mock,避免测试污染
因为prismaMock是共享对象,在每个测试前重置mock的实现和调用记录,确保测试用例之间相互独立:
beforeEach(async () => { // 清除所有mock的调用记录和实例 jest.clearAllMocks(); // 重置findUnique的默认返回值 prismaMock.user.findUnique.mockResolvedValue(null); const module: TestingModule = await Test.createTestingModule({ providers: [ UserService, { provide: PrismaService, useValue: prismaMock }, ], }).compile(); service = module.get<UserService>(UserService); });
方法三:用mockResolvedValue处理异步mock
对于异步方法,Jest提供了更清晰的mockResolvedValue方法来模拟成功的异步返回,替代mockReturnValue:
jest.spyOn(prismaMock.user, 'findUnique').mockResolvedValue(null);
内容的提问来源于stack exchange,提问作者Lucas César

