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

Jest测试中save方法未被调用的问题排查求助

Jest测试中Mock的save方法调用次数始终为0的问题

问题描述

我在验证Jest测试中被mock的save方法是否被调用时遇到问题:每次运行测试,均显示该方法的调用次数为0,但生产环境中一切正常,新数据可正常保存,ORM的save方法也能按预期被调用。

测试代码示例

describe('updateEmail', () => {
  it('should update user email', async () => {
    const user: User = {
      id: 'abc',
      email: 'old@email.org',
      password: 'password',
    };

    const updatedUser: User = Object.assign({}, user);
    updatedUser.email = 'new@email.org';

    const findOneSpy = jest
      .spyOn(repository, 'findOne')
      .mockResolvedValue(user);

    const saveSpy = jest
      .spyOn(repository, 'save')
      .mockResolvedValue(updatedUser);

    expect(
      service.updateEmail({
        oldEmail: user.email,
        newEmail: updatedUser.email,
      }),
    ).resolves.toEqual(updatedUser);

    expect(findOneSpy).toHaveBeenCalledWith({ where: { email: user.email } });

    expect(saveSpy).toHaveBeenCalledWith(updatedUser);
  });
});

业务逻辑代码

async updateEmail({ oldEmail, newEmail }: UpdateEmailDto) {
  const user = await this.findByEmail(oldEmail);

  if (!user) throw new NotFoundException();

  user.email = newEmail;

  return this.usersRepository.save(user);
}

报错信息

expect(jest.fn()).toHaveBeenCalledWith(...expected)

Expected: {"email": "new@email.org", "id": "abc", "password": "password"}

Number of calls: 0

  133 |       expect(findOneSpy).toHaveBeenCalledWith({ where: { email: user.email } });
  134 |
> 135 |       expect(saveSpy).toHaveBeenCalledWith(updatedUser);
      |                       ^
  136 |     });
  137 |   });
  138 | });

at Object.<anonymous> (users/users.service.spec.ts:135:23)

问题原因与解决办法

问题出在异步代码的处理时机和对象匹配逻辑:

  1. 调用service.updateEmail后,你直接用.resolves.toEqual断言,但没有等待异步操作完成就检查saveSpy的调用情况,此时save方法还未执行,导致调用次数为0。
  2. 业务代码中直接修改了findOne返回的原user对象的email属性,但测试里的updatedUser是新创建的对象,Jest的toHaveBeenCalledWith默认严格匹配对象引用,所以即使属性相同也会匹配失败。

具体修复步骤

  1. 等待异步操作完成:用await直接调用service.updateEmail,确保异步逻辑执行完毕后再做断言。
  2. 调整对象匹配方式:要么断言被修改后的原user对象,要么用expect.objectContaining做属性匹配,跳过对象引用的严格校验。

修复后的测试代码:

describe('updateEmail', () => {
  it('should update user email', async () => {
    const user: User = {
      id: 'abc',
      email: 'old@email.org',
      password: 'password',
    };

    const updatedEmail = 'new@email.org';

    const findOneSpy = jest
      .spyOn(repository, 'findOne')
      .mockResolvedValue(user);

    const saveSpy = jest
      .spyOn(repository, 'save')
      .mockResolvedValue({ ...user, email: updatedEmail });

    // 等待异步方法执行完成
    const result = await service.updateEmail({
      oldEmail: user.email,
      newEmail: updatedEmail,
    });

    expect(result).toEqual({ ...user, email: updatedEmail });
    expect(findOneSpy).toHaveBeenCalledWith({ where: { email: user.email } });
    
    // 方式1:用objectContaining匹配属性,忽略对象引用
    expect(saveSpy).toHaveBeenCalledWith(expect.objectContaining({
      email: updatedEmail,
      id: user.id
    }));

    // 方式2:直接断言被修改后的原对象(此时user的email已经被业务代码修改)
    // expect(saveSpy).toHaveBeenCalledWith(user);
  });
});

额外说明

  • 测试异步代码时,必须确保所有异步操作完成后再执行断言,否则会出现时序问题导致断言失败。
  • 当业务逻辑是修改原对象而非创建新对象时,优先用expect.objectContaining做部分匹配,避免对象引用不一致导致的断言失败。

内容的提问来源于stack exchange,提问作者nerrood

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 01:34:53