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

使用Jest测试嵌套Promise的createCampaign接口失败求助

问题描述

我正在测试的createCampaign函数采用嵌套Promise结构,目前无法改为async/await写法。该函数代码如下:

exports.createCampaign = (req, res) => {
  const { name } = req.body;
  User.findByPk(req.userId).then((user) => {
    Account.findOne({ where: { id: user.accountId } }).then((account) => {
      Campaign.create({
        accountId: account.id,
        name: name,
        uuid: uuid.v4(),
      })
        .then((campaign) => {
          if (campaign) {
            return res.status(201).json({
              id: campaign.id,
              name: campaign.name,
              uuid: campaign.uuid,
            });
          }
          return res.status(500).send({ message: 'Campaign not created' });
        })
        .catch((err) => {
          return res.status(500).send({ message: err.message });
        });
    });
  });
};

我编写的Jest测试用例如下:

jest.mock('@545490/kgr-models', () => ({
  user: {
    findByPk: jest.fn(),
  },
  account: {
    findOne: jest.fn(),
  },
  campaign: {
    create: jest.fn(),
    findOne: jest.fn(),
  },
}));

describe('CampaignController', () => {
  let mockStatus, mockJson, mockRes, mockSend;
  mockJson = jest.fn();
  mockSend = jest.fn();
  mockStatus = jest.fn(() => ({ json: mockJson, send: mockSend }));
  mockRes = { status: mockStatus };

  const mockUser = {
    id: 'userId',
    firstName: 'firstName',
    lastName: 'lastName',
    email: 'email',
    accountId: 'accountId',
  };
  const mockAccount = {
    id: 'accountId',
    name: 'accountName',
    partner: 'partner',
  };
  const mockCampaign = {
    id: 'campaignId',
    name: 'campaignName',
    accountId: 'accountId',
    status: 'active',
  };

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('createCampaign', () => {
    it('should create a campaign', async () => {
     
      User.findByPk.mockResolvedValue(mockUser);
      Account.findOne.mockResolvedValue(mockAccount);
      Campaign.create.mockResolvedValue(mockCampaign);

      await createCampaign({
        body: {
          name: 'campaignName',
        },
        userId: 'userId',
      }, mockRes);

      expect(User.findByPk).toHaveBeenCalledWith('userId');
      expect(Account.findOne).toHaveBeenCalledWith({ where: { id: mockUser.accountId } });
      expect(Campaign.create).toHaveBeenCalled();
      expect(mockStatus).toHaveBeenCalledWith(201);
      expect(mockJson).toHaveBeenCalled();
    });
  });
});

执行测试后出现失败,我怀疑是嵌套Promise的Mock方式不正确。尝试将原函数改为async/await结构后测试可正常通过,但目前不允许修改原函数,现求助如何正确测试该嵌套Promise结构的函数。

解决方案

问题核心是原函数没有返回完整的Promise链,导致Jest的await无法等待所有嵌套异步操作完成,测试会在内部Promise执行前就结束,断言自然失效。以下是两种可行的解决方式:

方式一:最小化修改原函数(仅添加return)

如果允许对原函数做极少量修改,只需要给每个嵌套的Promise链添加return,让整个异步流程的Promise被返回,这样测试中的await就能正确等待所有步骤完成:

exports.createCampaign = (req, res) => {
  const { name } = req.body;
  // 返回最外层Promise
  return User.findByPk(req.userId).then((user) => {
    // 返回Account查询的Promise
    return Account.findOne({ where: { id: user.accountId } }).then((account) => {
      // 返回Campaign创建的Promise
      return Campaign.create({
        accountId: account.id,
        name: name,
        uuid: uuid.v4(),
      })
        .then((campaign) => {
          if (campaign) {
            return res.status(201).json({
              id: campaign.id,
              name: campaign.name,
              uuid: campaign.uuid,
            });
          }
          return res.status(500).send({ message: 'Campaign not created' });
        })
        .catch((err) => {
          return res.status(500).send({ message: err.message });
        });
    });
  });
};

修改后原测试用例无需调整即可正常通过。

方式二:完全不修改原函数,调整测试用例

如果完全不能改动原函数,需要在测试中手动等待所有Mock的Promise执行完成后再断言,有两种可靠实现方式:

方法A:等待所有Mock的Promise resolve

it('should create a campaign', async () => {
  User.findByPk.mockResolvedValue(mockUser);
  Account.findOne.mockResolvedValue(mockAccount);
  Campaign.create.mockResolvedValue(mockCampaign);

  // 执行目标函数
  createCampaign({
    body: { name: 'campaignName' },
    userId: 'userId',
  }, mockRes);

  // 等待所有Mock的Promise完成
  await Promise.all([
    User.findByPk.mock.results[0].value,
    Account.findOne.mock.results[0].value,
    Campaign.create.mock.results[0].value
  ]);

  // 执行断言
  expect(User.findByPk).toHaveBeenCalledWith('userId');
  expect(Account.findOne).toHaveBeenCalledWith({ where: { id: mockUser.accountId } });
  expect(Campaign.create).toHaveBeenCalledWith({
    accountId: mockAccount.id,
    name: 'campaignName',
    uuid: expect.any(String) // 验证uuid格式
  });
  expect(mockStatus).toHaveBeenCalledWith(201);
  expect(mockJson).toHaveBeenCalledWith({
    id: mockCampaign.id,
    name: mockCampaign.name,
    uuid: expect.any(String)
  });
});

方法B:使用done回调配合setTimeout

it('should create a campaign', (done) => {
  User.findByPk.mockResolvedValue(mockUser);
  Account.findOne.mockResolvedValue(mockAccount);
  Campaign.create.mockResolvedValue(mockCampaign);

  // 执行目标函数
  createCampaign({
    body: { name: 'campaignName' },
    userId: 'userId',
  }, mockRes);

  // 等待事件循环清空,确保所有嵌套Promise执行完成
  setTimeout(() => {
    expect(User.findByPk).toHaveBeenCalledWith('userId');
    expect(Account.findOne).toHaveBeenCalledWith({ where: { id: mockUser.accountId } });
    expect(Campaign.create).toHaveBeenCalled();
    expect(mockStatus).toHaveBeenCalledWith(201);
    expect(mockJson).toHaveBeenCalled();
    done(); // 通知Jest测试完成
  }, 0);
});

关键原因说明

原函数的嵌套Promise没有被返回,导致createCampaign执行后立即返回undefined,测试里的await相当于等待一个非Promise值,不会等待内部异步操作结束。此时断言执行时,嵌套的Promise可能还没走到Campaign.create或res.status的步骤,导致断言失败。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 01:30:01