You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在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);
    });

问题根源

  1. Service业务逻辑缺陷:update方法中,查询得到updatedUser对象后,调用updateOne仅修改数据库数据,但直接返回的是内存中已存在的旧对象——数据库的修改不会自动同步到已查询的内存对象,因此返回值始终是原数据。
  2. Mock参数不匹配:Service调用updateOne时传入的参数是{ userName }(查询对象)和{ $set: { ...newDto } },但Mock的updateOne函数定义的参数是userName: string和newUserData: UpdateUserFieldsDTO,参数结构完全不匹配,导致Mock内部的修改逻辑根本没被触发。
  3. 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,提问作者Артем Яценко

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 15:33:15