在Nest.js中使用Jest正确模拟mongoose的updateOne方法问题
问题排查与修正方案
问题背景
在Nest.js应用中使用Jest测试mongoose的updateOne方法时出现异常:
UserService的update方法逻辑为:先调用findUserByUserName查询用户,再调用userModel.updateOne更新用户信息,但单元测试中,预期返回userName修改为"user"的对象,实际返回的仍是原userName为"userName"的对象,mock的updateOne内修改用户名的代码未生效。
相关代码
UserService核心方法
// UserService的update方法 public async update( newDto: UpdateUserFieldsDTO, userName: string, ): Promise<UserDocument> { const updatedUser = await this.findUserByUserName(userName); await this.userModel.updateOne({ userName }, { $set: { ...newDto } }); return updatedUser; } // findUserByUserName方法 public async findUserByUserName( userName: string, ): Promise<UserDocument | null> { return await this.userModel.findOne({ userName, deletedAt: null }); }
测试用Mock与用例
class MockedMongoModel { constructor(private user: CreateUserDTO) {} static readonly findOne = jest.fn( async (query: { userName?: string; _id?: string; email?: string }) => { if (query._id) { return query._id === mockUserObject._id ? mockUserObject : null; } else if (query.email) return query.email === mockUserObject.email ? mockUserObject : null; return query.userName === mockUser.userName ? mockUserObject : null; }, ); static readonly updateOne = jest.fn( async (userName: string, newUserData: UpdateUserFieldsDTO) => { const user = await this.findOne({ userName }); if (newUserData.userName) { user.userName = newUserData.userName; return user; } }, ); } // 测试用例 it.only("should update user's userName", async () => { const updatedUser = { ...mockUserObject, userName: "user", }; const expectedOutput = await service.update( { userName: "user" }, mockUser.userName, ); expect(MockedMongoModel.updateOne).toHaveBeenCalledTimes(1); expect(expectedOutput).toEqual(updatedUser); });
问题根源
- Service业务逻辑缺陷:
update方法中,查询得到updatedUser对象后,调用updateOne仅修改数据库数据,但直接返回的是内存中已存在的旧对象——数据库的修改不会自动同步到已查询的内存对象,因此返回值始终是原数据。 - Mock参数不匹配:Service调用
updateOne时传入的参数是{ userName }(查询对象)和{ $set: { ...newDto } },但Mock的updateOne函数定义的参数是userName: string和newUserData: UpdateUserFieldsDTO,参数结构完全不匹配,导致Mock内部的修改逻辑根本没被触发。 - Mock引用问题:即使参数匹配,Mock中修改的是
findOne返回的对象,但Service中的updatedUser是独立的内存引用,修改Mock内的对象不会影响Service返回的对象。
修正方案
1. 修复UserService的update方法
有两种可行方案:
方案一:更新后重新查询最新数据返回
public async update( newDto: UpdateUserFieldsDTO, userName: string, ): Promise<UserDocument> { await this.userModel.updateOne({ userName }, { $set: { ...newDto } }); // 更新后查询最新数据,若修改了userName则用新值查询,否则用原userName return this.findUserByUserName(newDto.userName || userName); }
方案二:直接修改文档实例并保存(mongoose文档支持)
public async update( newDto: UpdateUserFieldsDTO, userName: string, ): Promise<UserDocument> { const updatedUser = await this.findUserByUserName(userName); // 直接将新属性赋值给文档实例 Object.assign(updatedUser, newDto); // 调用save方法同步到数据库 await updatedUser.save(); return updatedUser; }
2. 修正测试代码
调整MockedMongoModel的参数匹配与逻辑
class MockedMongoModel { constructor(private user: CreateUserDTO) {} static readonly findOne = jest.fn( async (query: { userName?: string; _id?: string; email?: string }) => { if (query._id) { // 返回对象副本,避免引用修改影响原对象 return query._id === mockUserObject._id ? { ...mockUserObject } : null; } else if (query.email) return query.email === mockUserObject.email ? { ...mockUserObject } : null; return query.userName === mockUser.userName ? { ...mockUserObject } : null; }, ); static readonly updateOne = jest.fn( async (query: { userName: string }, update: { $set: UpdateUserFieldsDTO }) => { // 匹配Service调用时的参数结构 const user = await this.findOne(query); if (user && update.$set.userName) { user.userName = update.$set.userName; // 修改findOne的mock实现,让后续查询返回更新后的对象 this.findOne.mockImplementationOnce(async (q) => { if (q.userName === update.$set.userName) return user; return q.userName === query.userName ? user : null; }); return user; } }, ); }
修正测试用例(适配方案一的Service逻辑)
it.only("should update user's userName", async () => { const updatedUser = { ...mockUserObject, userName: "user", }; // 第一次findOne返回原对象,第二次返回修改后的对象 MockedMongoModel.findOne.mockResolvedValueOnce(mockUserObject).mockResolvedValueOnce(updatedUser); const expectedOutput = await service.update( { userName: "user" }, mockUser.userName, ); expect(MockedMongoModel.updateOne).toHaveBeenCalledTimes(1); // 验证updateOne的调用参数是否符合预期 expect(MockedMongoModel.updateOne).toHaveBeenCalledWith( { userName: mockUser.userName }, { $set: { userName: "user" } } ); expect(expectedOutput).toEqual(updatedUser); });
内容的提问来源于stack exchange,提问作者Артем Яценко
相关产品推荐
相关产品推荐

