jest: calling runAllTimers after using Lodash's _.debounce results in an infinite recursion error
Do you want to request a feature or report a bug?
bug
What is the current behavior?
When using fake timers, creating a debounced function, calling it a couple times, and then calling jest.runAllTimers
, an error will be printed:
Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out...
at FakeTimers.runAllTimers (node_modules/jest-util/build/FakeTimers.js:207:13)
at Object.<anonymous> (__tests__/lodash-bug-test.js:12:8)
It seems that changing the second argument passed to debounce (the time in milliseconds to debounce the function for) changes whether or not this error occurs. For example: on my machine (mid-2014 MBP) it appears to always throw when the delay is above ~600ms, but only fails some of the time when it’s around 500ms.
This issue has been encountered before (https://github.com/lodash/lodash/issues/2893), and it seems to have been on Lodash’s end. I added a comment to the issue in the Lodash repo, but @jdalton said that he’s not sure why it would still be occurring with recent versions of Lodash.
If the current behavior is a bug, please provide the steps to reproduce and either a repl.it demo through https://repl.it/languages/jest or a minimal repository on GitHub that we can yarn install
and yarn test
.
https://github.com/rimunroe/lodash-jest-timer-issue
What is the expected behavior?
The calling jest.runAllTimers
should cause the debounced function to behave as though the time it was told to debounce for elapsed.
Please provide your exact Jest configuration and mention your Jest, node, yarn/npm version and operating system.
I’m using macOS 10.12.4
No configuration other than calling jest.runAllTimers
in the test.
I encountered the bug with the following versions:
- node@4.4.7 with npm@2.15.8
- node@6.10.3 with npm@4.2.0 and yarn@0.23.4
- jest@18.1.0
- jest@19.0.1
- lodash@4.17.4
About this issue
- Original URL
- State: closed
- Created 7 years ago
- Reactions: 63
- Comments: 41 (13 by maintainers)
Commits related to this issue
- Fix issue with debounce https://github.com/facebook/jest/issues/3465 — committed to ONSdigital/eq-author-app by tgandrews 5 years ago
- Fix issue with debounce https://github.com/facebook/jest/issues/3465 — committed to ONSdigital/eq-author-app by tgandrews 5 years ago
- [tests] use jest mocks instead of sinon ones for timers Instead of using jest faketimers that don't seem to work directly, (maybe because https://github.com/facebook/jest/issues/3465) just mock `_.th... — committed to ToucanToco/weaverbird by adimasci 5 years ago
- [tests] use jest mocks instead of sinon ones for timers Instead of using jest faketimers that don't seem to work directly, (maybe because https://github.com/facebook/jest/issues/3465) just mock `_.th... — committed to ToucanToco/weaverbird by adimasci 5 years ago
- Mock lodash debounce to avoid test instability https://github.com/facebook/jest/issues/3465 — committed to lizozom/kibana by deleted user 5 years ago
- Mock lodash debounce to avoid test instability (#44592) https://github.com/facebook/jest/issues/3465 — committed to elastic/kibana by deleted user 5 years ago
- Mock lodash debounce to avoid test instability (#44592) https://github.com/facebook/jest/issues/3465 — committed to lizozom/kibana by deleted user 5 years ago
- Mock lodash debounce to avoid test instability (#44592) (#44627) https://github.com/facebook/jest/issues/3465 — committed to elastic/kibana by deleted user 5 years ago
- Add SearchBar unit tests The <SearchBar /> component debounces searches. Normally that would be tested using Jest's fake timers, however due to this issue https://github.com/facebook/jest/issues/3465... — committed to Ultimaker/react-web-components by matijs 5 years ago
- Add SearchBar unit tests The <SearchBar /> component debounces searches. Normally that would be tested using Jest's fake timers, however due to this issue https://github.com/facebook/jest/issues/3465... — committed to Ultimaker/react-web-components by matijs 5 years ago
- Upgrade JSDom, Babel, and Jest to resolve _.debounce blowing up in test. See https://github.com/facebook/jest/issues/3465 — committed to everydayhero/react-widgets by HowlingEverett 4 years ago
- Fix db.test. - Fake timers cause an infinite loop on _.debounce. - Jest v26 contains a 'modern' option for useFakeTimers, but create-react-app uses an older version of jest https://github.com/faceboo... — committed to cybersemics/em by raineorshine 4 years ago
I was able to get around this by mocking lodash’s
debounce
moduleMerged a fix 1 day before the issue’s 3 year anniversary 😅 Available in
jest@26.0.0-alpha.1
viajest.useFakeTimers('modern')
.next
docs: https://jestjs.io/docs/en/next/jest-object#jestusefaketimersimplementation-modern--legacy@rimunroe I had the same error when using
jest.runAllTimers()
. I switched tojest.runOnlyPendingTimers()
and this fixed my problem.jest.runOnlyPendingTimers() eliminates the error message for me, but the method is never invoked.
Managed to work around this with a combination of
jest.useRealTimers()
,setTimeout
anddone
.for those who don’t want to read the whole thread
throttle
usesdebounce
inside, that usesDate.now()
, so not only timers should be faked, but the Date API too.Lodash throttles by way of debounce. It’s robust and handles things like clock drift after daylights savings time. That said, IMO it’s not really Lodash’s burden to prop up a mock library. We do our part to be good neighbors and don’t hold on to timer references like
setTimeout
. Beyond that it really depends on your level of mock. For example, you could mock the debounced function itself instead of the underlying timer apis.I used this to mock lodash’s debounce, it works for most of my use cases: place the following in
__mocks__/lodash/debounce/index.js
in your root project directorythen just use it with jest’s timer mocking and your tests should behave correctly. as always, extend as appropriate 😃
Btw, I hit this too. Turns out lodash’s implementation of throttle is way more complex than it (imo) should be. Ended up with a simple helper like this one because I didn’t have time to debug what’s wrong.
I still needed debounce behavior in my tests, so mocking debounce to return the function wouldn’t work. But I also didn’t need the level of robustness that _.debounce provides. Mocking lodash’s robust debounce with a naive debounce fit my needs. I put this in my project:
__mocks__/lodash/debounce.js
I had a case where I wanted to test component which used
_.debounce
in several places and I had to mock implementation of only one usage, I’ve done it in following way:Hope this will help someone
Opened up #5165 for it.
I just mocked it so debounce returns the passed function like so:
jest.mock('lodash/debounce', () => fn => fn);
This worked in my particular case where I was calling the function directly in the test anyway but no good if you need to actually test that the debounce functionality itself works by calling it multiple times…
I believe I know what’s going on here.
To implement its voluminous functionality,
throttle
(which is essentially a wrapper arounddebounce
) passes control between a series ofsetTimeout
calls. These calls handle various circumstances when it could be time to invoke the wrapped function (leading edge, trailing edge, etc). One of the placessetTimeout
is used is here:This function looks at the current time, and decides if it’s time to invoke the wrapped function. It assumes that the timeout function is actually occurring
delay
milliseconds in the future. In this case, that’s not true, because Jest does not mock the current time. Here’s an excerpt of what Jest does:This is a tight loop, which is why @tleunen (and I) observe
delay
values that are declining slowly (498ms
remaining,497ms
, etc). And it explains why @rimunroe (and I) noticed that the “infinite timeouts” Jest error is only thrown above certainwait
times. If it takes JestX
milliseconds to run through the timeout execution loop shown above100k
times, then throttle will work if the wait is less thanX
.Lodash reads the current time from
Date.now
. Jest can fix this by mocking outDate
, and making the timeout execution loop something like:Got it to work with sinonjs fake timers. Here’s a small sample:
Thanks @xevrem! But the debounced function is not being passed the correct arguments, I suggest updating your example with this (accept and spread
args
):I am having this same issue. Using sinon’s fake timers I am able to advance the clock and test that a debounced function is called. I am trying to convert to Jest, and using Jest’s fake timers, I get
Ran 100000 timers, and there are still more!
when usingjest.runAllTimers
, and the function is not invoked when usingjest.runOnlyPendingTimers
orjest.runTimersToTime
.I am able to use real timers and do something similar to @jkaipr above:
little addition to @xevrem version here (adding args support):
Please open up a new issue with a minimal reproduction if you’re stilling having issues
Another basic mock similar to @xevrem answer but with a mocked .flush() as well, in case you need that.
Also note if you are just importing the
lodash.debounce
the mock goes in__mocks__/lodash.debounce/index.js
+1