-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes to debounce() causing test failures with mocked timers #2918
Comments
Thanks for reaching out @ilikesleeping. There is no intentional breaking change, but I'm not ruling out an unintentional one. Although I currently don't see yet how that should have happened. The changed logic in #2913 just restores some tests that have been in Underscore for a very long time, exactly meant to confirm that you can't tamper with the timeout intervals of Could you show me your code that has broken because of the update? That might help me diagnose what is causing the issue. Thanks in advance. |
Thanks for the quick and detailed reply. I've added a simplest possible test case here to show the difference between the two versions. Just The jest test code runs through any timers set with Obviously it's not your job to support this kind of odd use case in other libraries, but I wonder if retaining a reference to the Date.now function at the time the debounce is created, rather than when the library is instantiated, would be a reasonable middle ground, as it is at least more predictable that way? |
@ilikesleeping Thank you for the reduced testcase, it's very informative. I think I now understand how #2892 broke your tests. I did some historical research as well and found more answers. I will not change the logic, for reasons that I'll explain below, but I think there is a different way to fix your tests, which I'll also detail below. In chronological order, here are some relevant things that happened to
All in all, Underscore 1.12.1 has finally brought However, if you are doing CommonJS imports in your application and your tests, like in the reduced testcase that you sent, then there is hope. You can override var _ = require('underscore');
var _1_12_0 = require('underscore-1.12.0');
_.mixin({debounce: _1_12_0.debounce});
// all code invoking _.debounce after this point will use the 1.12.0 version If you don't want to have two complete copies of Underscore in your test runtime, you can also avoid this by using our modular CommonJS build: var _ = require('underscore');
var _1_12_0_debounce = require('underscore-1.12.0/cjs/debounce.js');
_.mixin({debounce: _1_12_0_debounce}); I have demonstrated this workaround in the context of your reduced testcase over here: https://github.com/ilikesleeping/underscore-debounce-test/pull/1 |
Thanks for the response and history of debounce, very informative. It seems our implementation was perhaps working by chance all along. Of course prioritising the real-world use cases of course makes sense, so we shall find a way to work around it. Closing this issue out as there's nothing left to do here. Thanks again! |
@ilikesleeping Thanks for closing. If I can help finding workarounds (for example for module systems other than CommonJS), please let me know. |
@jgonggrijp Is there a recommended way to test these then? Importing an older version of the library to use a specific function in tests while using a newer version in production doesn't really seem like a very maintainable approach. Other time-related functions such as |
@Sacharified thanks for reaching out.
Please keep in mind that I'm not suggesting that you swap out the entire library for an older version. I'm only suggesting to replace That said, I'd be more than happy to suggest an approach that might suit you better. Please tell me more about your particular use case if you'd like that. |
@jgonggrijp Thanks for the response I want to understand regardless of my use case: What's the recommended way to test a function which implements
Here's my issues with this: I don't want to have to include 2 different versions of Underscore in my project. I don't want to have a different implementation in my tests to my production code and I definitely don't want to ship 2 different versions of the library to the browser.
In my test I have a text input component which automatically calls an API endpoint when typed in to. I have a debounce on the search function so that it doesn't call the API whilst the user is still typing. I want to validate that when my test enters a 2+ character query the API is only called once. I don't think you can really mock that without basically re-implementing the debounce function. I could just allow the real timers to run but I don't want to slow down my test suite. |
@Sacharified So here's the thing. In your application in production, you're debouncing something, which implies a substantial delay. If you don't want to have that delay in your unittests, you're always going to somehow make your unittests different from the application in production. You could fake the time, change logic, lower the debounce interval, or a combination of such measures. In all cases, whatever approach you take to effectively make your unittest less realistic, you won't be shipping those modifications in production.
Honestly, I don't think I could recommend a single approach that works for everyone. However, bear with me and keep reading.
Again, there is no need to ship two versions of Underscore in production. Unittests always involve dependencies that aren't present in production. Using a second version of Underscore just for monkeypatching
If you really want to completely eliminate any debounce delay, then this is your only option. However, you can save yourself the effort of actually re-implementing
Maybe you can reach an acceptable compromise by adjusting the debounce interval for your unittests. For example, you could have the key presses arrive at 1 ms intervals and delay the API call until 5 ms pass without typing. Depending on how you set the interval in production, this could already be a hundredfold speedup even though it doesn't completely eliminate the delay. As a final remark, I would like to mention that there is a fine line between unittesting your own logic and unittesting your own glue code that basically just invokes a library function. In the latter case, you end up indirectly unittesting the library function, which in most cases is wasted effort because the library already has unittests of its own. As you describe your use case above, it seems a bit like you're just unittesting whether (Of course, you also want to test whether all of your units and your external dependencies integrate correctly, which implies testing the glue code as well. This is however the topic of functional testing, and I wouldn't generally recommend messing with the time in functional tests. Functional testing is very different from unittesting.) |
For what it's worth, I ultimately solved my issue by switching to use this debounce instead, which I believe is the implementation from lodash. That one works just fine with mocked timers. (Sorry @jgonggrijp!) |
@ilikesleeping The Readme of that package states that it was extracted from Underscore, but I wouldn't mind either way. It's your project. If you are using Underscore in production and also that alternative debounce implementation, do realize that your users are downloading two implementations of |
underscore > 1.12.0 breaks debounce compatibility with mocked timers jashkenas/underscore#2918
In v1.12.1, it seems the
debounce
method has changed to usenow()
, which holds onto a reference to whatever Date.now was when underscore was loaded the first time.This has caused problems in our jest tests, where we're using
jest.useFakeTimers
to mock changes in time to test timeouts and suchlike, because any mocked changes to the date functions won't have any effect.Looking at the tests added at the same time, it seems this was perhaps intentional? If so, I think it's possibly not a suitable change for a .1 patch release, as it'll break anything that was relying on mocking the current time - which I suspect is a reasonably common use case in tests. (Ok, maybe not that common, but not totally out there...)
Thanks!
The text was updated successfully, but these errors were encountered: