jest: doc : jest fake timers : expect on setTimeout not working

šŸ› Bug Report

In https://jestjs.io/fr/docs/timer-mocks, we can see that we can assert that setTimeout has been called once : expect(setTimeout).toHaveBeenCalledTimes(1);

However, if you do this in a test, jest will complain :

expect(received).toHaveBeenCalledTimes(expected)

Matcher error: received value must be a mock or spy function

Received has type:  function
Received has value: [Function setTimeout]

  216 |     //TODO: test refresh works
> 217 |     expect(setTimeout).toHaveBeenCalledTimes(1);
      |                        ^
  218 |   });
  219 | });
  220 |

  at Object.<anonymous> (xxx.spec.ts:217:24)

I think the documentation should be fixed to explain how we can do ā€¦

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Comments: 19 (6 by maintainers)

Most upvoted comments

I confirm that I also get ReferenceError: setTimeout is not defined in 27.0.3, the scenario is as follows:

afterEach(jest.useRealTimers);

it('test A', () => {
   jest.useFakeTimers();
   jest.spyOn(global, 'setTimeout');

   // run code which calls setTimeout
   expect(setTimeout).toHaveBeenCalledTimes(1);
   expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 1000);
});

it('test B', () => {
   // run code which calls setTiemout

   // perform other assertions not related to setTimeout on same code as exercised in test A
  expect(...);
  expect(...);
}

Test A passes, but code executed by Test B fails, console.log(setTimeout) in that code returns undefined. If I remove the spy on Test A, then Test B passes.

@sigveio , not testing setTimeout, but a callback instead as you mention in previous comments is not an option for me. My setTimeout performs a recursive call to the same function, which is not exposed. Something like:

module.exports = function() {

    const recursiveFunc = function() {

        const expire = ....;
         
        setTimeout(recursiveFunc, expire);

    }

}

Why wouldnā€™t I be able to spy on a global function? I understand how this could lead to testing internals of an implementation that might not contribute to a proper unit test, but thatā€™s a decision a developer should be able to make rather than having the testing framework force this decision upon them.

I went by all the reports about it not working and thought that perhaps it was sacrificed for the fact that relying on an external library greatly simplifies things for Jest.

But actually, I was partially wrong and should have tested it more thoroughly.

After you have enabled the fake timers you can spy on the global:

jest.spyOn(global, 'setTimeout');

That said; I do still stand by my comment on it most often being more favourable not to do so.

Side note: Specifically what Iā€™d like to still be able to do is assess whether certain calls happened in an expected order. I donā€™t much care about the exact processor time that elapses but rather the information that events A, B, and C happened before event D.

When you use the modern fake timers, ā€œprocessor timeā€ should not play into the millisecond timing of when a given task can be expected to run though, because time is entirely faked. So with for example jest.advanceTimersByTime() you do have a lot of power.

I would also think that tasks under fake timers would run in the natural order they are scheduled in. So if you want to ignore the exact timingā€¦ and only care about the orderā€¦ then perhaps you can use jest.runAllTimers() to fast forward in time and exhaust all the queues, and then toHaveBeenNthCalledWith() to verify them?

How is one supposed to solve this issue?

My tests start to fail as described in the inital report (i.e. I get a ā€œreceived value must be a mock or spy functionā€ error when invoking expect(setTimeout).not.toHaveBeenCalled() in a test).

Adding jest.spyOn(window, 'setTimeout') inexplicably produces a ā€œReferenceError: setTimeout is not definedā€ error:

node:internal/process/promises:246
          triggerUncaughtException(err, true /* fromPromise */);
          ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "ReferenceError: setTimeout is not defined".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

Iā€™m using testEnvironment: 'jsdom'. The function window.setTimeout does exist in the test, so I donā€™t really understand how it can appear as not defined to the test runner.

Iā€™m experiencing a very strange return of this issue in the same project as before.

Iā€™ve made changes to my TypeScript source code (effectively adding 2 await statements to function calls) and doing so causes the jest to crash when running the tests:

 RUNS  src/poll.test.ts
node:internal/process/promises:246
          triggerUncaughtException(err, true /* fromPromise */);
          ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "ReferenceError: setTimeout is not defined".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

The underlying error is once more ā€œReferenceError: setTimeout is not definedā€. The tests donā€™t run at all. The test(ā€¦) blocks are completely unchanged and start off with the line jest.spyOn(global, 'setTimeout'). Removing it stops jest from crashing butā€”very much expectedlyā€”causes my tests to fail. This happens on Jest 27 using fake timers and JSDOM as the test environment.

Reproduction steps:

  1. Clone https://github.com/kleinfreund/poll
  2. Run npm install
  3. Run npm test to see the tests pass
  4. Change src/poll.ts#L18 to if (await shouldStopPolling()) { (note the added await)
  5. Run npm test to see the tests crash

Since this issue is tagged with ā€œneeds reproā€, here is a repro.

I copied the example from the docs exactly, and setTimeout is not mocked. When I use legacy timers, the documented example works as expected. This suggests that the documentation demonstrates the legacy timers, not the modern timers. That document was last updated 8 months ago, and the commit history doesnā€™t seem to suggest that the document was changed since the migration to modern timers.

I canā€™t actually find a document on the jest site for modern timers. Hopefully this reflects my own inability to find the right search terms, rather than that jest has migrated to an undocumented timer mock API?

if you are using jest 27, it uses modern timers now by default you will need to spy on window.setTimeout beforeHands