jestjs / jest

Delightful JavaScript Testing.
https://jestjs.io
MIT License
44.33k stars 6.47k forks source link

Provide a way to find out whether fake timers have been enabled. #10555

Open 9still opened 4 years ago

9still commented 4 years ago

🚀 Feature Proposal

Provide a method (e.g. jest.usingFakeTimers()) that returns true if jest.useFakeTimers was previously invoked. Alternatively, a property or a config object could be exposed with the same information.

Motivation

It is often convenient to have a global cleanup routine for tests that does things like always reverting to real timers, etc. It would be awesome if we could flush timers (e.g. jest.runOnlyPendingTimers()) in this cleanup routine as well, but to do that, one would need to find out if fake timers are actually being used.

There are already examples of libraries resorting to introspection of the setTimeout implementation to try to deduce this for similar purposes.

@kentcdodds had to do the following https://github.com/testing-library/react-testing-library/pull/720/files recently

function getIsUsingFakeTimers() {
  return (
    typeof jest !== 'undefined' &&
    typeof setTimeout !== 'undefined' &&
    (setTimeout.hasOwnProperty('_isMockFunction') ||
      setTimeout.hasOwnProperty('clock'))
  )
}

Would be awesome if jest exposed an official method to check the above, so that tooling wouldn't break if the underlying implementation were to change.

Example

afterEach(() => {
  if (jest.usingFakeTimers()) {
    jest.runOnlyPendingTimers();
  }
});

Pitch

Why does this feature belong in the Jest core platform?

Since jest.useFakeTimers is a part of the core platform, it seems reasonable to be able to find out whether that call had previously been invoked. The core platform is the only place that would be able to provide this information.

cschwebk commented 3 years ago

I think something similar can be achieved by doing the following:

afterEach(() => {
  if (jest.isMockFunction(setTimeout)) {
    jest.runOnlyPendingTimers();
    jest.useRealTimers();
  }
});

https://jestjs.io/docs/jest-object#jestismockfunctionfn

kentcdodds commented 3 years ago

I do exactly this: https://github.com/kentcdodds/bookshelf/blob/d4851afda9ae40feb61ca28f76e957290ed2bfab/src/setupTests.js#L52-L55

9still commented 3 years ago

Thanks for the suggestion @cschwebk ! That seems like a very clean workaround, but it'd be awesome if it was officially supported/documented, since strictly speaking without that, it's not guaranteed that enabling fake timers will make setTimeout an actual mock function and could thus break at any time if the underlying implementation changes.

CreativeTechGuy commented 2 years ago

It's worth noting that these methods aren't actually correct. If you use jest.spyOn(global, "setTimeout"), the checks above will still be true despite the fact that timers aren't actually using the fake implementations. So simply checking if setTimeout is mocked isn't enough to definitively determine.

I was not able to find any way to differentiate between a spy and a mock so I'm not sure how it'd be possible to determine this reliably, even in a hacky way.

If only there was a property/method exposed by Jest which could help us! 😉

mdtusz commented 2 years ago

This seems not to work with jest 28.1.0 - jest.isMockFunction(setTimeout) will always return false, regardless of using real or fake timers.

As a temporary and hacky workaround that is almost certain to break, checking the setTimeout.name property seems to be an indication of whether the timers are mocked, but this will be extremely brittle long term. When the timers are mocked, setTimeout.name === "setTimeout", and when using real timers, setTimeout.name === undefined.

I can't explain why this is the case (maybe some setup from jest-environment-jsdom?), as setTimeout.name === "setTimeout" in browsers as well as node, but it seems to work for the time being.

To maybe provide some reasoning for why this feature is useful, it can be used with the react-testing-library user-event companion library. Setup of this is something like:

userEvent.setup();

But when using mocked timers, you must provide an advanceTimers function to hook into jest

userEvent.setup({ advanceTimers: jest.advanceTimersByTime });

To simplify this, it is often put in a setup wrapper, so should be able to dynamically set the correct advanceTimers function based on whether or not the running test context has mocked time.

vkarpov15 commented 2 years ago

I can confirm that jest.isMockFunction(setTimeout) doesn't work in Jest 29 either, always returns false even if fake timers enabled :( I ended up doing the following.

if (typeof jest !== 'undefined' && setTimeout.clock != null && typeof setTimeout.clock.Date === 'function') {
  // ...
}

Can you please consider deprecating useFakeTimers() ?

github-actions[bot] commented 1 year ago

This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

CreativeTechGuy commented 1 year ago

Not stale

ogonkov commented 1 year ago

jest@29.7.0 use @sinonjs/fake-timers internally to fake timers, they detect fake timers by checking global.Date.isFake, i guess it safe for now to do the same, to check for fake timers.

https://github.com/sinonjs/fake-timers/blob/0f2861015f3fab85d36367281395ee94a7eac4fe/src/fake-timers-src.js#L1731

declare global {
    interface DateConstructor {
        /* Jest uses @sinonjs/fake-timers, that add this flag */
        isFake: boolean;
    }
}

const hasFakeTimers = global.Date.isFake === true;
github-actions[bot] commented 1 week ago

This issue is stale because it has been open for 1 year with no activity. Remove stale label or comment or this will be closed in 30 days.

vkarpov15 commented 1 week ago

Bump. Please deprecate useFakeTimers().