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

使用Sinon/Chai验证函数首次调用参数的问题求助

问题

单元测试中匹配函数调用参数时遇到失败,测试代码如下:

beforeEach(() => {
  enrollmentOffer = offerReadModel(EnrollmentOfferStatuses.Accepted, false, undefined, true)
  fakeSearcher = buildFakeReadModelSearcher()
  ...
})
afterEach(() => {
  sinon.restore()
})

it('does the right query to get the latest child offer', async () => {
  await expect(calculateRenewInfo(enrollmentOffer as EnrollmentOfferReadModel, true)).to.be.fulfilled
  expect(fakeSearcher.filter).to.have.been.calledWithExactly({
    offerType: { eq: OfferTypes.Child },
    enrollmentOfferId: { eq: enrollmentOffer.id },
  })
})

测试报错信息:

22) calculateRenewInfo
     does the right query to get the latest child offer:
   AssertionError: expected filter to have been called with exact arguments {
offerType: { eq: 'child' },
enrollmentOfferId: { eq: '0a1a6e82-c2ca-48ec-9548-c4b5e4d26653' }
}
{ enrollmentOfferId: { eq: '0a1a6e82-c2ca-48ec-9548-c4b5e4d26653' } } {
offerType: { eq: 'child' },
enrollmentOfferId: { eq: '0a1a6e82-c2ca-48ec-9548-c4b5e4d26653' }
} 

问题核心:filter函数被调用了两次,第一次参数符合预期,第二次参数仅包含enrollmentOfferId,导致全局匹配失败。尝试使用firstCall属性时,TypeScript报错提示该属性不存在,因为类型系统无法识别fakeSearcher.filter是Sinon创建的fake函数。

fakeSearcher的构建代码:

export function buildFakeReadModelSearcher<TReadModel = unknown>(): Partial<Searcher<TReadModel>> {
  const fakeSearcher: Partial<Searcher<TReadModel>> = {}
  fakeSearcher.filter = sinon.fake.returns(fakeSearcher)
  fakeSearcher.afterCursor = sinon.fake.returns(fakeSearcher)
  fakeSearcher.paginatedVersion = sinon.fake.returns(fakeSearcher)
  fakeSearcher.limit = sinon.fake.returns(fakeSearcher)
  fakeSearcher.sortBy = sinon.fake.returns(fakeSearcher)
  fakeSearcher.search = sinon.fake.resolves({ items: [] })
  fakeSearcher.searchOne = sinon.fake.resolves(undefined)
  return fakeSearcher
}

需求:在尽量不修改buildFakeReadModelSearcher的前提下,仅验证filter首次调用的参数。

解决方案

方法1:类型断言访问firstCall

通过类型断言将fakeSearcher.filter转换为sinon.SinonFake类型,即可使用firstCall属性验证首次调用:

it('does the right query to get the latest child offer', async () => {
  await expect(calculateRenewInfo(enrollmentOffer as EnrollmentOfferReadModel, true)).to.be.fulfilled
  // 断言为SinonFake类型,解锁fake函数专属属性
  const filterFake = fakeSearcher.filter as sinon.SinonFake
  expect(filterFake.firstCall).to.have.been.calledWithExactly({
    offerType: { eq: OfferTypes.Child },
    enrollmentOfferId: { eq: enrollmentOffer.id },
  })
})

方法2:直接读取调用历史数组

Sinon的fake函数会将所有调用记录存储在callHistory数组中,通过类型断言后可直接取第一个元素的参数进行验证:

it('does the right query to get the latest child offer', async () => {
  await expect(calculateRenewInfo(enrollmentOffer as EnrollmentOfferReadModel, true)).to.be.fulfilled
  const filterFake = fakeSearcher.filter as sinon.SinonFake
  // 验证首次调用的第一个参数
  expect(filterFake.callHistory[0].args[0]).to.deep.equal({
    offerType: { eq: OfferTypes.Child },
    enrollmentOfferId: { eq: enrollmentOffer.id },
  })
})

方法3:结合调用次数限制验证

如果只需确保首次调用符合预期,可同时验证调用次数和参数,确保目标参数是第一次调用的传参:

it('does the right query to get the latest child offer', async () => {
  await expect(calculateRenewInfo(enrollmentOffer as EnrollmentOfferReadModel, true)).to.be.fulfilled
  const filterFake = fakeSearcher.filter as sinon.SinonFake
  // 验证目标参数被调用过一次(即首次调用)
  expect(filterFake).to.have.been.calledWithExactly({
    offerType: { eq: OfferTypes.Child },
    enrollmentOfferId: { eq: enrollmentOffer.id },
  }).once
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 08:07:55