使用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
相关产品推荐
相关产品推荐

