Search code examples
javascriptunit-testingchaisinonsinon-chai

Testing arguments of first call with Sinon/Chai


I am trying to match the arguments of functions calls for my unit tests.

The test code is below:

  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 },
    })
  })

Currently this unit-test errs with:

  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' }
} 

The reason the test fails, I believe, is because there are two calls. The first internal invocation is matching my test arguments. While the second, with arguments being { enrollmentOfferId: { eq: '0a1a6e82-c2ca-48ec-9548-c4b5e4d26653' } }, is not matching it.

Normally, I would do something like: expect(sinonFake.firstCall).to.have.been.calledWith(...). This is however not working for me. For example, if I write expect(fakeSearcher.filter.firstCall).to.have.been.calledWithExactly(...), there is an error Property 'firstCall' does not exist on type '(filters: FilterFor<OfferReadModel>) => Searcher<OfferReadModel>'.

We build fakeSearcher with:

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
}

Any suggestions/ideas how I could only match the first call, with no or minimal changes to fakeSearcher?


Solution

  • The problem is that buildFakeReadModelSearcher says it returns a Partial<Searcher<TReadModel>> so Typescript doesn't know that fakeSearcher.filter is a sinon fake.

    You could switch the function to return an object containing all those fakes + the fakeSearcher object as well and then use those fakes directly.

    export function buildFakeReadModelSearcher<TReadModel = unknown>() {
      const fakeSearcher: Partial<Searcher<TReadModel>> = {}
      const filter = sinon.fake.returns(fakeSearcher)
      const afterCursor = sinon.fake.returns(fakeSearcher)
      const paginatedVersion = sinon.fake.returns(fakeSearcher)
      const limit = sinon.fake.returns(fakeSearcher)
      const sortBy = sinon.fake.returns(fakeSearcher)
      const search = sinon.fake.resolves({ items: [] })
      const searchOne = sinon.fake.resolves(undefined)
    
      fakeSearcher.filter = filter
      fakeSearcher.afterCursor = afterCursor
      fakeSearcher.paginatedVersion = paginatedVersion
      // ...etc.
    
      return { fakeSearcher, filter, afterCursor, paginatedVersion, limit, sortBy, search, searchOne }
    }
    
    // Then use it like so:
    expect(filter.firstCall).to.have.been.calledWithExactly({
      offerType: { eq: OfferTypes.Child },
      enrollmentOfferId: { eq: enrollmentOffer.id },
    })