Pete
Pete

Reputation: 12553

Get sinon to resolve a promise when stubbed function is called

I have a set of integration tests for message queueing code (over a real RabbitMQ). The tests are verifying that the correct use case is triggered when a specific message arrives.

it("should trigger the use case", () => {
  const stub = sinon.stub(app, 'useCaseToTrigger').resolves();
  await publishMessage({ id: "SOME_ID" })
  // FAIL - Message isn't processed yet.
  stub.should.have.been.calledWith(match({id: "SOME_ID" }))
})

This doesn't work. The await statement waits for the message to be published, but says nothing of when the message is processed.

I can add a delay, but that quickly becomes a huge waste of time (if there are multiple tests).

it("should trigger the use case", () => {
  const stub = sinon.stub(app, 'useCaseToTrigger').resolves();
  await publishMessage({ id: "SOME_ID" })
  await new Promise(r => setTimeout(r, 100));
  // PASS - the message has been processed before the verification
  stub.should.have.been.calledWith(match({id: "SOME_ID" }))
})

But if I make the stubbed function resolve a promise when called, I can get my test to work without any undue delay.

it("should trigger the use case", () => {
  const promise = new Promise(r => {
    sinon.stub(app, "useCaseToTrigger").callsFake(() => {
      setImmediate(() => { r(); }) // Push to the end of the event loop
      return Promise.resolve();
    })
  })
  await publishMessage({ id: "SOME_ID" })
  await promise;
  // PASS - Message processed before continuing test
  app.useCaseToTrigger.should.have.been.calledWith(match({id: "SOME_ID" }))
})

This does become a bit cumbersome to deal with (and probably not the easiest to read either). But it also gives eslint floating promises warnings. So to avoid those warnings, I need to rewrite it to:

it("should trigger the use case", () => {
  await Promise.all([
    new Promise(r => {
      sinon.stub(app, "useCaseToTrigger").callsFake(() => {
        setImmediate(() => { r(); }) // Push to the end of the event loop
        return Promise.resolve();
      })
    }),
    publishMessage({ id: "SOME_ID" })
  ]);
  // PASS - Message processed before continuing test
  app.useCaseToTrigger.should.have.been.calledWith(match({id: "SOME_ID" }))
})

Is there a way to make this cleaner.

Upvotes: 1

Views: 1455

Answers (1)

Matt
Matt

Reputation: 74680

I think you can use an empty promise to await, so the app logic eventually resolves that promise when it is called. Sinon provides a promise fake that makes this a bit easier than creating a new Promise with an external reference to the resolve function.

const p = sinon.promise()
const stub = sinon.stub(app, 'useCaseToTrigger').callsFake(p.resolve)
await publishMessage({ id: "SOME_ID" })
await p
sinon.assert.calledWith(stub, { id: "SOME_ID" })

It looks like sinon .resolves() uses Promise.resolve at the time of the stub call, so it doesn't have a reference to the eventual promise at stub creation, otherwise a stub.promise type of reference might have been useful here to avoid the additional promise setup.

Upvotes: 2

Related Questions