97

I'm really confused trying to create test with the help of Jest documentation https://facebook.github.io/jest/docs/timer-mocks.html#content

I'm trying to check a state when container mounts and then few seconds later, after I have manually set values in the state (using setTimeout()).

I have a function inside Main's componentDidMount like this:

componentDidMount() {
    this.setStateAfterDelay();
}

And what the function does is:

setStateAfterDelay = () => {
    setTimeout(() => {
        this.setState({ fruits: ['banana', 'apple', 'orange', 'vodka', 'kiwi'] });
    }, 1500);
}

I achieved the first part with:

const component = mount(<Main />);
expect(component.state().fruits).toEqual(null);

But I have no clue how to check the state again after, lets say 2000ms?

Any help is appreciated :)

1
  • function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } await delay(1000) Commented Oct 11, 2024 at 1:01

5 Answers 5

155

while jest can run async code easily, you can use promise and setTimeout combination to wait a bit. For example this code will wait for 2 real seconds (if want to mock time check jest docs):

await new Promise((r) => setTimeout(r, 2000));

Full sample test. Don't forget to add async flag before the callback function:

test('some test title', async () => {
  const foo = true;
  await new Promise((r) => setTimeout(r, 2000));
  expect(foo).toBeDefined();
});

Also, keep in mind that default "timeout" is 5 seconds (5000ms). If your test may run longer, you can add jest.setTimeout(30000); above the test(). 30000 will make sure to not timeout for 30 seconds. You can add any number you need. Full example with setTimeout:

jest.setTimeout(30000);

test('some test title', async () => {
  const foo = true;
  await new Promise((r) => setTimeout(r, 2000));
  expect(foo).toBeDefined();
});

You can add timeout in jest config too. Here are config docs

Sign up to request clarification or add additional context in comments.

9 Comments

whats the difference to FakeTimers?
FakeTimers, as name hints, are fake timers. setTimeout will wait for real seconds. Read the docs here: jestjs.io/docs/timer-mocks
I followed this approach with React component with useState. Testing-library warned me to use act(...) and I did let timeout = null; await act(async () => { await new Promise((r) => { timeout = setTimeout(r, 2000) }) });. Then I need to use useEffect(()=> { return () => { cleanTimeout(timeout) } }, [timeout]) to solve all warnings.
IMO, if you know that the result won't be there for at least 2 seconds, useFakeTimers() is better solution since you know when to check. Instead of wasting time in your test waiting. All unit tests should run fast, why wait?
@Akrikos updated with link to mock time docs. Yes, i agree waiting real time is not ideal but in some cases it's needed. Of course tests should be pure functions and not have side effects but stuff do happen and usually i needed myself real time wait, as always it depends on situation what's "normal usage". in any case, added link to mock time too.
|
57

I haven't really tested this code. But, something similar to this should work I think.

const fruits = ['banana', 'apple', 'orange', 'vodka', 'kiwi'];

it('mock setTimeout test', () => {
 jest.useFakeTimers();
 setTimeout(() => {
   expect(component.state().fruits).toEqual(fruits);
 }, 1500);
 jest.runAllTimers();
});

4 Comments

Yes, this seems to work good! I thought that there was some crazy syntax with unit-testing to achieve this. Thanks!
does this expect really runs? I dont see debug pointer hitting here but my test case going green.
This doesn't seem to work. Where are you mounting?
Because of setTimeout this test wont run but it wont fail also and u see green in test coverage
21

You don't need to delay your tests, simply calling jest.runAllTimers() before asserting, will work.

const fruits = ['banana', 'apple', 'orange', 'vodka', 'kiwi'];

it('initializes the fruits state', () => {
 jest.useFakeTimers();
 jest.runAllTimers();
 expect(component.state().fruits).toEqual(fruits);
});

You could also call useFakeTimers() in a beforeEach if you are going to test multiple times and also runAllTimers() could be inside another beforeEach so you don't repeat yourself.

Comments

3

I know this is question about how to check something after 20 seconds. But this might be also an indicator that you don't want to test 20 seconds, since what's important sometimes is whether certain action has been performed with the right inputs. In this case, you could restructure your code a bit so that you can pass in a dispatch function. For instance

    function abc() {
        return dispatch => {
            return Promise.then(res => {})  // this would take 20 seconds
        }
    }

Because dispatch is passed in, therefore you can easily use the following in the testing code.

    const dispatch = Jest.fn()
    abc(dispatch)
    expect(dispatch).toBeCalled()

Of course the assumption is that you do not care about if it's 20 seconds, instead you care more about the workflow process.

Comments

2

I've struggled with this for a while because questions/answers about this problem are often unprecise.

In the case where you are running jest with jest.useFakeTimers(), you have then have two types of time:

  • Real time (wall clock), that one can't be faked.
  • Fake time (mocked) that is resulting from the jest call to useFakeTimers.

In this case, doing something "after a delay" isn't that clear. Are we talking about a real delay, or a fake delay?

Advancing fake time

This is the easier part it has been answered in many SO posts and github issues, for example, patching calls to advanceTimersByTime:

export async function advanceTimersByTime(msToRun: number) {
    jest.advanceTimersByTime(msToRun);
    await flushPromises();
}

function flushPromises() {
    return new Promise(jest.requireActual('timers').setImmediate);
}

Which can then be used to check if a long lasting task inside your app managed to perform correctly:

describe('Tests', () => {
    it('async test', async () => {
        startLongTask();
        await advanceTimersByTime(10_MINUTES);
        // Assert everything went according to specifications
    });
});

Advancing real time (when using fake timers)

The problem arise when you want to advance real time after you called jest.useFakeTimers(). I've tried many approaches, here is the one that worked for me.

In my use case (testing node-cron based scheduler) I had no need for setInterval to be mocked by jest, therefore, I could simply tell jest to not patch it:

jest.useFakeTimers({
    doNotFake: ['nextTick', 'setImmediate', 'setInterval'],
});

Remarak, using nextTick seems to be required (otherwise tests will timeout).

Using this, setInterval is now based on real time, which will allow us to define an elapseRealTime function using it:

export async function elapseRealTime(ms: number) {
    return new Promise<void>((resolve) => {
        const intervalId = setInterval(async () => {
            resolve();
            clearInterval(intervalId); // mimic setTimeout
        }, ms);
    });
}

This sounds dumb, but it works. Now we can use elapseRealTime in a test to advance real time.

describe('Tests', () => {
    it('async test', async () => {
        startBackgroundTask();
        await advanceTimersByTime(10_MINUTES);
        // Eventhough fake time now advanced 10 minutes, the background task
        // had no time to be executed if its not part of our app
        // (it is using real time and is unaware of our fake timers)
        await elapseRealTime(10_MS);
        // Now the background task had 10 milliseconds to perform some
        // work.
    });
});

If you need to mock setIntervall in your tests

In the case where you need setIntervall to be mocked, an alternative solution (that I don't like that much) is to not mock Date:

jest.useFakeTimers({
    doNotFake: ['nextTick', 'setImmediate', 'Date'],
});

And use an active loop that regularly check the current time. In our case, this can then be used to advance real time:

function elapseRealTime(ms: number) {
    const now = new Date().getTime();
    const then = now + ms;
    while(new Date().getTime() < then){ 
        /* Do nothing */ 
    }
}

Since this one is synchronous, it will lock every other test out of the event loop (so it will cost you CPU time and make your test suite run for longer). That also mean you need to call it without await:

describe('Tests', () => {
    it('async test', async () => {
        startBackgroundTask();
        await advanceTimersByTime(10_MINUTES);
        elapseRealTime(10_MS);
    });
});

Why would you need both types of delays

You might need to wait for a fake delay if you are testing so internal application time-related events (a cron scheduling a given task after a delay for example).

But you might also be interested in real delay for example to give enough time for another application (or a database/sensor/...) to achieve a task related to the test you are running (integration tests). In my use case I needed another app to trigger web hooks calls.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.