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:
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.
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.
@jdalton thanks for your input!
Since this is a recursive issue you might look into a non-recursive mock that uses a while loop and a queue array of setTimeout
invocations to walk through. I've done something similar in the past when mocking out setTimeout
to be a synchronous queue.
@rimunroe I had the same error when using jest.runAllTimers()
. I switched to jest.runOnlyPendingTimers()
and this fixed my problem.
jest.runOnlyPendingTimers() eliminates the error message for me, but the method is never invoked.
I have the same issue, no error message but the method is not invoked.
Managed to work around this with a combination of jest.useRealTimers()
, setTimeout
and done
.
it('debounces the prop', done => {
wrapper.prop('debounced')('arg1', 'arg2');
wrapper.prop('debounced')('arg3', 'arg4');
wrapper.prop('debounced')('arg5', 'arg6');
setTimeout(() => {
expect(props.debounced.calls.count()).toBe(1);
expect(props.debounced).toHaveBeenCalledWith('arg5', 'arg6');
done();
}, 1000);
});
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 using jest.runAllTimers
, and the function is not invoked when using jest.runOnlyPendingTimers
or jest.runTimersToTime
.
I am able to use real timers and do something similar to @jkaipr above:
it('triggers debounce', (done) => {
//set up and call debounced function
setTimeout(() => {
//test function was called
done()
}, 300 /* however long my debounce is */)
})
I was able to get around this by mocking lodash's debounce
module
import debounce from 'lodash/debounce'
import { someFunction, someDebouncedFunction } from '../myModule';
jest.mock('lodash/debounce', () => jest.fn(fn => fn));
...
it('tests something', () => {
debounce.mockClear();
someFunction = jest.fn();
someDebouncedFunction();
expect(debounce).toHaveBeenCalledTimes(1);
expect(someFunction).toHaveBeenCalledTimes(1);
});
Since lodash doesn鈥檛 use timers for it鈥檚 denounce and throttle I assume this particular issue is not actionable from our side and should be closed.
What do you mean by not using timers?
@thymikee The issue is this.
That鈥檚 fine with me
@thymikee
Since lodash doesn鈥檛 use timers for it鈥檚 denounce and throttle I assume this particular issue is not actionable from our side and should be closed.
What timers does Lodash not use?
Lodash uses setTimeout
for debounce
and throttle
.
I'm guessing the issue with jest is it has a problem with mocking setTimeouts
that themselves call setTimeout
.
For those wanting alternative mocks you might look at sinon.js/lolex.
@jdalton looks like I misunderstood what you wrote earlier, sorry about that 馃槄.
Fake timers support running recursive setTimeouts, but Jest bails out after 100000 calls. Although lodash may hit some edge case.
Should we look into just integrating lolex instead of rolling our own fake timer implementation?
@SimenB That would be rad!
Opened up #5165 for it.
I shouldn't have closed this in the first place, so I'm reopening it.
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:
const originalDebounce = _.debounce;
jest.spyOn(_, 'debounce').mockImplementation((f, delay) => {
// can also check if f === yourDebouncedFunction
if (delay === constants.debounceTime) {
return f;
}
return originalDebounce(f, delay);
});
Hope this will help someone
I'm having the same issue with the 100,000 limit reached and I've found out that the lodash implementation of debounce is creating a ton of timers.
When debouncing a fn with a wait time of 500ms, I can see multiple timers with wait values of 498, then multiple 497 and so on... So I'm not especially surprised that we can reach 100k.
@jdalton is that expected that lodash creates so many timers?
It calls leadingEdge
first, and then loop until 0 in timerExpired
I believe I know what's going on here.
To implement its voluminous functionality, throttle
(which is essentially a wrapper around debounce
) passes control between a series of setTimeout
calls. These calls handle various circumstances when it could be time to invoke the wrapped function (leading edge, trailing edge, etc). One of the places setTimeout
is used is here:
function timerExpired() {
var time = now();
// Handle the case where we should invoke now
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Handle the case where we're not done yet.
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}
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:
for (i = 0; i < this._maxLoops; i++) {
const nextTimerHandle = this._getNextTimerHandle();
this._runTimerHandle(nextTimerHandle);
}
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 certain wait
times. If it takes Jest X
milliseconds to run through the timeout execution loop shown above 100k
times, then throttle will work if the wait is less than X
.
Lodash reads the current time from Date.now
. Jest can fix this by mocking out Date
, and making the timeout execution loop something like:
for (i = 0; i < this._maxLoops; i++) {
const nextTimerHandle = this._getNextTimerHandle();
// Update mocked time
Date.now += getExpiry(nextTimerHandle)
this._runTimerHandle(nextTimerHandle);
}
Hmm. I have no idea but that issue just rises randomly today. One of tests starts failing after year of work without errors. 馃
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..
+1
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
export default function simpleDebounce(fn, delay) {
let timer = null;
return function wrappedFunction(...args) {
const context = this;
clearTimeout(timer);
timer = setTimeout(function invokeFunction() {
fn.apply(context, args);
}, delay);
};
}
for those who don't want to read the whole thread
throttle
uses debounce
inside, that uses Date.now()
, so not only timers should be faked, but the Date API too.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 directory
export const debounce = jest.fn().mockImplementation((callback, timeout) => {
let timeoutId = null;
const debounced = jest.fn(()=>{
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(callback, timeout);
});
const cancel = jest.fn(()=>{
window.clearTimeout(timeoutId);
});
debounced.cancel = cancel;
return debounced;
});
export default debounce;
then just use it with jest's timer mocking and your tests should behave correctly. as always, extend as appropriate :)
Thanks @xevrem! But the debounced function is not being passed the correct arguments, I suggest updating your example with this (accept and spread args
):
const debounced = jest.fn((...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => callback(...args), timeout);
});
Merged a fix 1 day before the issue's 3 year anniversary 馃槄 Available in [email protected]
via jest.useFakeTimers('modern')
. next
docs: https://jestjs.io/docs/en/next/jest-object#jestusefaketimersimplementation-modern--legacy
Got it to work with sinonjs fake timers. Here's a small sample:
import FakeTimers from '@sinonjs/fake-timers';
let clock;
describe('some tests', () => {
beforeEach(() => {
clock = FakeTimers.createClock();
});
it('should do a thing', () => {
const fakeWaitInMillis = 5000;
// call func that uses debounce
clock.setTimeout(() => {
// expect func to be called after wait
}, fakeWaitInMillis);
});
});
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
const debounce = jest.fn().mockImplementation((callback, delay) => {
let timer = null;
let pendingArgs = null;
const cancel = jest.fn(() => {
if (timer) {
clearTimeout(timer);
}
timer = null;
pendingArgs = null;
});
const flush = jest.fn(() => {
if (timer) {
callback(...pendingArgs);
cancel();
}
});
const wrapped = (...args) => {
cancel();
pendingArgs = args;
timer = setTimeout(flush, wrapped.delay);
};
wrapped.cancel = cancel;
wrapped.flush = flush;
wrapped.delay = delay;
return wrapped;
});
export default debounce;
Most helpful comment
I was able to get around this by mocking lodash's
debounce
module