Skip to content
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

feat(jest-fake-timers): Add feature to enable automatically advancing… #15300

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

atscott
Copy link

@atscott atscott commented Sep 11, 2024

… timers

Summary

Testing with mock clocks can often turn into a real struggle when dealing with situations where some work in the test is truly async and other work is captured by the mock clock.

In addition, when using mock clocks, testers are always forced to write tests with intimate knowledge of when the mock clock needs to be ticked. Oftentimes, the purpose of using a mock clock is to speed up the execution time of the test when there are timeouts involved. It is not often a goal to test the exact timeout values. This can cause tests to be riddled with manual advancements of fake time. It ideal for test code to be written in a way that is independent of whether a mock clock is installed or which mock clock library is used. For example:

document.getElementById('submit');
// https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))

When mock clocks are involved, the above may not be possible if there is some delay involved between the click and the request to the API. Instead, developers would need to manually tick the clock beyond the delay to trigger the API call.

This commit attempts to resolve these issues by adding a feature which allows jest to advance timers automatically with the passage of time, just as clocks do without mocks installed.

Test plan

I wrote some unit tests but let me know if you need more.

Copy link

linux-foundation-easycla bot commented Sep 11, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

  • ✅ login: atscott / name: Andrew Scott (de306d5)

Copy link

netlify bot commented Sep 11, 2024

Deploy Preview for jestjs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit de306d5
🔍 Latest deploy log https://app.netlify.com/sites/jestjs/deploys/66e324f36e5b79000809fe9c
😎 Deploy Preview https://deploy-preview-15300--jestjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@atscott atscott force-pushed the autoAdvance branch 2 times, most recently from 835635d to fb18fa9 Compare September 11, 2024 23:07
@mrazauskas
Copy link
Contributor

Is the behaviour of this new method different from the advanceTimers option? If so, perhaps it would be good to mention that in documentation?

jest/docs/JestObjectAPI.md

Lines 883 to 889 in bd1c6db

type FakeTimersConfig = {
/**
* If set to `true` all timers will be advanced automatically by 20 milliseconds
* every 20 milliseconds. A custom time delta may be provided by passing a number.
* The default is `false`.
*/
advanceTimers?: boolean | number;

@atscott
Copy link
Author

atscott commented Sep 12, 2024

Is the behaviour of this new method different from the advanceTimers option? If so, perhaps it would be good to mention that in documentation?

Good question! Yes, it's quite different, both in correctness and in practicality. I'll update the documentation with some version of this explanation (and please correct me if any of this seems wrong):

advanceTimers is essentially setInterval(() => clock.tick(ms), ms) while this feature is const loop = () => setTimeout(() => clock.nextAsync().then(() => loop()), 0);

There are two key differences between these two:

  1. advanceTimers uses clock.tick(ms) so it synchronously runs all timers inside the "ms" of the clock queue. This doesn't allow the microtask queue to empty between the macrotask timers in the clock whereas something like tickAsync(ms) (or a loop around nextAsync) would. This could arguably be considered a fixable bug in advanceTimers
  2. advanceTimers uses real time to advance the same amount of real time in the mock clock. The way I understand it, this feels somewhat like "real time with the opportunity to advance more quickly by manually advancing time". setAdvanceTimersAutomatically would be quite different: It advances time as quickly possible and as far as necessary. Without manual ticks, advanceTimers would only be capabale of automatically advancing as far as the timeout of the test and take the whole real time of the test timeout. In contrast, setAdvanceTimersAutomatically can theoretically advance infinitely far, limited only by processing speed. Somewhat similar to the --virtual-time-budget feature of headless chrome.

Given that advanceTimers already exists in the config, it feels quite reasonable to me that this new feature should have a more unique name.

… timers

Testing with mock clocks can often turn into a real struggle when dealing with situations where some work in the test is truly async and other work is captured by the mock clock.

In addition, when using mock clocks, testers are always forced to write tests with intimate knowledge of when the mock clock needs to be ticked. Oftentimes, the purpose of using a mock clock is to speed up the execution time of the test when there are timeouts involved. It is not often a goal to test the exact timeout values. This can cause tests to be riddled with manual advancements of fake time. It ideal for test code to be written in a way that is independent of whether a mock clock is installed or which mock clock library is used. For example:

```
document.getElementById('submit');
// https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
await waitFor(() => expect(mockAPI).toHaveBeenCalledTimes(1))
```

When mock clocks are involved, the above may not be possible if there is some delay involved between the click and the request to the API. Instead, developers would need to manually tick the clock beyond the delay to trigger the API call.

This commit attempts to resolve these issues by adding a feature which allows jest to advance timers automatically with the passage of time, just as clocks do without mocks installed.
@mrazauskas
Copy link
Contributor

Interesting. Do I get it right that this new API is somewhat a variation of jest.advanceTimersToNextTimerAsync()? Or? I thought, perhaps instead of adding new one, it would be enough to extend the existing API:

  • jest.advanceTimersToNextTimerAsync('auto'),
  • jest.advanceTimersToNextTimerAsync('manual').

@atscott
Copy link
Author

atscott commented Sep 16, 2024

Do I get it right that this new API is somewhat a variation of jest.advanceTimersToNextTimerAsync()?

Yes, that's right!

I thought, perhaps instead of adding new one, it would be enough to extend the existing API:

  • jest.advanceTimersToNextTimerAsync('auto'),
  • jest.advanceTimersToNextTimerAsync('manual').

I think that's a pretty good idea for an alternative place for this API. Would jest.advanceTimersToNextTimerAsync('auto'/'manual') both advance the timer and update the internal setting/behavior? Or would it just change the behavior auto/manual behavior without advancing? It could be undesirable to not have the ability to change the behavior without also advancing to the next timer.

@mrazauskas
Copy link
Contributor

The idea I was talking about was to replace jest.setAdvanceTimersAutomatically(true) with jest.advanceTimersToNextTimerAsync('automatic') and setAdvanceTimersAutomatically(false) with jest.advanceTimersToNextTimerAsync('manual'). Or some such. Just an idea.

@atscott
Copy link
Author

atscott commented Sep 17, 2024

The idea I was talking about was to replace jest.setAdvanceTimersAutomatically(true) with jest.advanceTimersToNextTimerAsync('automatic') and setAdvanceTimersAutomatically(false) with jest.advanceTimersToNextTimerAsync('manual'). Or some such. Just an idea.

Ah, you mean that it the automatic/manual would entirely replace the existing steps parameter there? It might be worth considering, though I was hoping we could introduce this without making it a breaking change.

@atscott
Copy link
Author

atscott commented Sep 19, 2024

@mrazauskas happy to continue iterating on the API naming. Would you also be able to help with the CI failures? I’m not sure what’s going on there.

@mrazauskas
Copy link
Contributor

Hm.. I think those failures are not related with your code. GHA run all the tests and if they pass, I think all is fine.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants