jestjs / jest

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

Provide an API to flush the Promise resolution queue #2157

Open hon2a opened 7 years ago

hon2a commented 7 years ago

Do you want to request a feature or report a bug?

Feature, I guess, but a pretty important one when testing code that uses Promises.

What is the current behavior?

I have a component that uses Promise wrapping and chaining internally in the follow-up to an external asynchronous action. I'm providing the mock of the async action and resolving the promise it returns in my test.

The component something like this:

class Component extends React.Component {
  // ...
  load() {
    Promise.resolve(this.props.load())
      .then(
        result => result
          ? result
          : Promise.reject(/* ... */)
        () => Promise.reject(/* ... */)
      )
      .then(result => this.props.afterLoad(result));
  }
}

And the test code looks something like this:

const load = jest.fn(() => new Promise(succeed => load.succeed = succeed));
const afterLoad = jest.fn();
const result = 'mock result';
mount(<Component load={load} afterLoad={afterLoad} />);
// ... some interaction that requires the `load`
load.succeed(result);
expect(afterLoad).toHaveBeenCalledWith(result);

The test fails because the expect() is evaluated before the chained promise handlers. I have to replicate the length of the inner promise chain in the test to get what I need, like this:

return Promise.resolve(load.succeed(result))
  // length of the `.then()` chain needs to be at least as long as in the tested code
  .then(() => {})
  .then(() => expect(result).toHaveBeenCalledWith(result));

What is the expected behavior?

I'd expect Jest to provide some sort of an API to flush all pending promise handlers, e.g.:

load.succeed(result);
jest.flushAllPromises();
expect(result).toHaveBeenCalledWith(result);

I've tried runAllTicks and runAllTimers to no effect.


Alternatively, if I'm just missing some already existing feature or pattern, I'm hoping for someone here to point me in the right direction :)

thesmart commented 3 years ago

@thesmart and that solution (await Promise.resolve();) works when using modern, no matter of the amount of promises that need to resolve in a chain?

Not sure, honestly. Probably would need to create some test cases for either modern or legacy as part of a patch. The default is now modern (see: sinonjs) but I'm not sure what version of Jest aligned w/ the test case above. Whatever the documented workaround is, it should note what to do for modern and/or legacy.

dobradovic commented 3 years ago

Maybe this is off topic but I really need some help with mocking async / await functions.

I want to test this async / await function using jest/enzyme

 deleteAndReset = async () => {

    await this.submitDeleteMeeting();
    this.props.resetMeetingState();
  }

which occurs on

<BackButton
            data-test="backButton"
             onClick: this.deleteAndReset 
          />

What I tried

describe('Delete and reset on click', () => {

  const props = {
    intl,
    deleteAndReset: jest.fn(),
    submitDeleteMeeting: jest.fn()
  };

  it('on click call deleteAndReset', async () => {
    component = shallow(<StartMeeting {...props} />);

    const backButton = findByDataTestAttr(component, 'backButton');

    expect(backButton.exists()).toEqual(true);

    const instance = component.instance();

    backButton.props().onClick();

    await Promise.resolve(); 

    expect(instance.props.submitDeleteMeeting).toHaveBeenCalled();
  });
});

This is what I got - ``` "expect(jest.fn()).toHaveBeenCalled()

Expected number of calls: >= 1
Received number of calls:    0"

What I am doing wrong with async / await testing?

Sorry for a little bit longer post but this "simple" thing drives me crazy for long time. 

Any advice and suggest would be helpful, thanks for your time! :)
hon2a commented 3 years ago

@dobradovic It is off-topic indeed. I'd recommend StackOverflow, unless you'd like to report a bug, in which case a new, separate issue would do the trick.

jwbay commented 3 years ago

This solution was broken for the JSDOM environment in Jest 27, via the removal of setImmediate: https://github.com/facebook/jest/issues/2157#issuecomment-279171856

Removal: https://github.com/facebook/jest/pull/11222

An equal implementation of waitForPromises now gets a lot trickier, complicated further by the fact that setImmediate in JSDOM apparently bypassed fake timers. So moving to another timer means we need to handle faking.

One solution is switching to setTimeout, and temporarily forcing real timers. For illustration:

return new Promise(resolve => {
  jest.useRealTimers();
  setTimeout(() => resolve(), 0);
  jest.useFakeTimers();
});

Now we obviously don't want to force fake timers if they weren't fake already. I seem to be able to tell whether timers are faked via jest.isMockFunction(setTimeout) ✔️. However, I can't tell which timer style is currently being used; legacy vs. modern ❌. For a widely used function to suddenly need this context to work is quite painful.

@SimenB short of a fully supported jest.waitForPromises, would you instead consider a new Jest API like:

jest.withRealTimers(() => {
  // code here is always run with real timers
});
// original timer state restored -- whether real, legacy, or modern

This could be nice even once legacy is removed, since the test code doesn't need to try and detect the current timer state.

Alternately, has anyone else worked around this differently? I tried process.nextTick and that works some of the time for legacy, but modern timers mock nextTick.

stephenh commented 3 years ago

@jwbay we've been using requireActual:

export function flushPromises(): Promise<void> {
  return new Promise(jest.requireActual("timers").setImmediate);
}
acontreras89 commented 2 years ago

The following implementation was working just fine (and still does when used with legacy timers):

global.flushPromises = () => new Promise(process.nextTick)

However, it is not working with modern timers because process.nextTick is also mocked. I am curious to know why, since @sinonjs/fake-timers does not do this by default (see the default value of config.toFake)

zyf0330 commented 2 years ago

With idea from @acontreras89 , I don't need to flushPromises, just let Jest doNotFake nextTick to make Promise works.

peter-bloomfield commented 1 year ago

I've hit a similar problem where I'm trying to test code which automatically retries a series of asynchronous operations. It uses a combination of promises and timers internally, and can't always expose a promise or callback to signal completion.

Based on the suggestion by @stephenh, here's a utility function I wrote to help my tests wait for all pending timers and promises:

const waitForPromisesAndFakeTimers = async () => {
  do {
    jest.runAllTimers();
    await new Promise(jest.requireActual('timers').setImmediate);
  } while (jest.getTimerCount() > 0);
};

The loop is there to catch timers which were scheduled following resolution of other promises. It definitely isn't suitable for all situations (e.g. recursive timers), but it's useful in some cases.

EduardoSimon commented 1 year ago

@peter-bloomfield Thanks a lot! Your snippet worked like a charm. I have the exact same situation, I have a fetch implementation that retries the requests using setTimeout and wrapping them in a promise each time. I wanted to extract the value out of the promise so I had to await it again:

const requestPromise = request(requestUrl)
await waitForPromisesAndFakeTimers();

const { status } = await requestPromise

expect(status).toEqual('success');
github-actions[bot] commented 6 months 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.

odinho commented 6 months ago

Still think this makes sense as a util helper. I've used flushPromises() in all projects since I started following this.