How to use JEST to test the time course of recursive functions - javascript

I write a test using JEST. I do not know how to test promise recursion in JEST.
In this test, the retry function that performs recursion is the target of the test until the promise is resolved.
export function retry<T>(fn: () => Promise<T>, limit: number = 5, interval: number = 1000): Promise<T> {
return new Promise((resolve, reject) => {
fn()
.then(resolve)
.catch((error) => {
setTimeout(() => {
// Reject if the upper limit number of retries is exceeded
if (limit === 1) {
reject(error);
return;
}
// Performs recursive processing of callbacks for which the upper limit number of retries has not been completed
try {
resolve(retry(fn, limit - 1, interval));
} catch (err) {
reject(err);
}
}, interval);
});
});
}
Perform the following test on the above retry function.
retry() is resolved in the third run. The first, second and third times are called every 1000 seconds respectively.
I thought it would be as follows when writing these in JEST.
jest.useFakeTimers();
describe('retry', () => {
// Timer initialization for each test
beforeEach(() => {
jest.clearAllTimers();
});
// Initialize timer after all tests
afterEach(() => {
jest.clearAllTimers();
});
test('resolve on the third call', async () => {
const fn = jest
.fn()
.mockRejectedValueOnce(new Error('Async error'))
.mockRejectedValueOnce(new Error('Async error'))
.mockResolvedValueOnce('resolve');
// Test not to be called
expect(fn).not.toBeCalled();
// Mock function call firs execution
await retry(fn);
// Advance Timer for 1000 ms and execute for the second time
jest.advanceTimersByTime(1000);
expect(fn).toHaveBeenCalledTimes(2);
// Advance Timer for 1000 ms and execute for the third time
jest.advanceTimersByTime(1000);
expect(fn).toHaveBeenCalledTimes(3);
await expect(fn).resolves.toBe('resolve');
});
});
As a result, it failed in the following error.
● retry › resolve on the third call
Timeout - Async callback was not invoked within the 30000ms timeout specified by jest.setTimeout.Error:
> 16 | test('resolve on the third call', async () => {
| ^
17 | jest.useFakeTimers();
18 | const fn = jest
19 | .fn()
I think that it will be manageable in the setting of JEST regarding this error. However, fundamentally, I do not know how to test promise recursive processing in JEST.

Your function is so hard to testing with timer.
When you call await retry(fn); this mean you will wait until retry return a value, but the setTimeout has been blocked until you call jest.advanceTimersByTime(1000); => this is main reason, because jest.advanceTimersByTime(1000); never been called.
You can see my example, it is working fine with jest's fake timers.
test("timing", async () => {
async function simpleTimer(callback) {
await callback();
setTimeout(() => {
simpleTimer(callback);
}, 1000);
}
const callback = jest.fn();
await simpleTimer(callback); // it does not block any things
for (let i = 0; i < 8; i++) {
jest.advanceTimersByTime(1000); // then, this line will be execute
await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
}
expect(callback).toHaveBeenCalledTimes(9); // SUCCESS
});
I think, you could skip test timer detail, just test about you logic: fn has been called 3 times, finally it return "resolve"
test("resolve on the third call", async () => {
const fn = jest
.fn()
.mockRejectedValueOnce(new Error("Async error"))
.mockRejectedValueOnce(new Error("Async error"))
.mockResolvedValueOnce("resolve");
// expect.assertions(3);
// Test not to be called
expect(fn).not.toBeCalled();
// Mock function call firs execution
const result = await retry(fn);
expect(result).toEqual("resolve");
expect(fn).toHaveBeenCalledTimes(3);
});
Note: remove all your fake timers - jest.useFakeTimers

The documentation of jest suggests that testing promises is fairly straightforward ( https://jestjs.io/docs/en/asynchronous )
They give this example (assume fetchData is returning a promise just like your retry function)
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});

I agree with #hoangdv this recursive setTimeout pattern is very tricky to test. This is the only solution that worked for me after trying #hoangdv's (which gave me much inspiration).
Note I'm using a similar retry pattern around network Fetches. I'm using React Testing Library to look for an error message that only appears when the retry function ran 3 times.
it('Shows file upload error on timeout', async () => {
jest.useFakeTimers();
setupTest();
// sets up various conditions that trigger the underlying fetch
while (
screen.queryByText(
'There appears to be a network issue. Please try again in a few minutes.'
) === null
) {
jest.runOnlyPendingTimers(); // then, this line will be execute
await Promise.resolve(); // allow any pending jobs in the PromiseJobs queue to run
}
await waitFor(() => {
expect(
screen.getByText(
'There appears to be a network issue. Please try again in a few minutes.'
)
).toBeInTheDocument();
});
jest.useRealTimers();
});

Related

Jest with async function calls before and after setTimeout

I have a main function which calls two async functions with sleep function in between. This is a basic example:
index.js
const func1 = async() => {
setTimeout(()=>{console.log('func 1...')}, 1000);
}
const func2 = async() => {
setTimeout(()=>{console.log('func 2...')}, 1000);
}
const sleep = ms => {
console.log(`Sleeping for ${ms/1000} seconds`);
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
const main = async() => {
try {
await func1();
// Sleeping for a long long time
console.log('Before Sleep');
await sleep(2000000);
console.log('After Sleep')
await func2();
return 'success';
} catch(err) {
console.log(err);
return 'error'
}
}
And this is my test code:
index.test.js
const index = require('./index');
jest.useFakeTimers();
describe('Testing index.js...', () => {
test('Should return success', async() => {
const promise = index();
jest.advanceTimersByTime(2000000);
promise.then(response => {
expect(response).toBe('success');
})
});
})
The test passes, but the console shows the following:
func 1...
Before Sleep
Sleeping for 2000 seconds
I tried the same this, but with func1() and func2() being synchronous functions:
const func1 = () => {
console.log('func 1...');
}
const func2 = () => {
console.log('func 2...');
}
const sleep = ms => {
// Sleeping for a long time
console.log(`Sleeping for ${ms/1000} seconds`);
return new Promise(resolve => {
setTimeout(resolve, ms);
})
}
const main = async() => {
try {
func1();
// Sleeping for a long long time
console.log('Before Sleep');
await sleep(2000000);
console.log('After Sleep')
func2();
return 'success';
} catch(err) {
console.log(err);
return 'error'
}
}
In that case, the test passes and the logs are also as expected:
func 1...
Before Sleep
Sleeping for 2000 seconds
After Sleep
func 2...
In the same synchronous code, if I make func1 async (keeping func2 synchronous), the problem reappears.
If func1 is synchronous and func2 is async, everything works as expected.
I have also tried using jest.runAllTimers() and jest.runOnlyPendingTimers(). I have also tried using async-await in the test file, but that (understandably) gives a timeout error:
index.test.js using async-await
const index = require('./index');
jest.useFakeTimers();
describe('Testing index.js...', () => {
test('Should return success', async() => {
const promise = index();
jest.advanceTimersByTime(3000000);
const response = await promise;
expect(response).toBe('success');
});
})
Console:
func 1...
Before Sleep
Sleeping for 2000 seconds
Error:
Timeout - Async callback was not invoked within the 5000ms timeout specified by jest.setTimeout.Timeout
How can I make this work?
I have gone through and tried a solutions on lot of Github issues in jest, and also a lot of questions on stack overflow, none of the solutions seem to work.
I am using jest 25.5.4
Edit: I also tried increasing the value in jest.advanceTimersBytTime() to a day. And also tried making the function in describe async.
I've had a similar issue recently, what worked for me is to advance the timers from within an async-call. Seems like jest does not support setting the timers within a promise (see https://github.com/facebook/jest/pull/5171#issuecomment-528752754). Try doing:
describe('Testing index.js...', () => {
it('Should return success', () => {
const promise = main();
Promise.resolve().then(() => jest.advanceTimersByTime(2000005));
return promise.then((res) => {
expect(res).toBe('success');
});
});
});
async, raw promises and done callback shouldn't be used together in tests. This is a common sign that a developer isn't fully comfortable with Jest asynchronous testing, this leads to error-prone tests.
The problem with original is that promise.then(...) promise is ignored because it's not chained. Asynchronous test should return a promise in order for it to be chained.
func1 and func2 return promises that resolve immediately and produce one-tick delay rather than 1 second delay. This should be taken into account because otherwise there's a race condition with setTimeout being called after advanceTimersByTime.
It should be:
test('Should return success', async() => {
const promise = index();
await null; // match delay from await func1()
jest.advanceTimersByTime(2000000);
const response = await promise;
expect(response).toBe('success');
});

stub setTimeout function

I have a function wait
async function wait(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
And I call this wait like this: await wait(5000); from a different function.
I am writing unit test cases and it always executes wait and each test case waits for 5s.
How do I stub the setTimeout using Sinon?
I tried:
// Skip setTimeOut
clock = sinon.useFakeTimers({
now: Date.now(),
toFake: ['setTimeout']
});
await clock.tickAsync(4000);
await Promise.resolve();
But it didn't work.
Related post: setTimeout not triggered while using Sinon's fake timers
Github issue: https://github.com/sinonjs/fake-timers/issues/194#issuecomment-395224370
You can solve this in two ways.
Consider whether your test case requires a delay of 5000ms.
The unit test should test the code logic, it's not integration tests. So, maybe you just need to make an assertion check the wait function is to be called with parameter. It's enough. We don't need to wait for 5000ms delay in the test case.
If you insist want to use sinon.useFakeTimers() and clock.tick(5000).
From the related post, we can do it like this:
index.ts:
async function wait(time: number, clock?) {
return new Promise((resolve) => {
setTimeout(resolve, time);
clock && clock.tick(time);
});
}
export async function main(time, /* for testing */ clock?) {
await wait(time, clock);
console.log('main');
}
index.test.ts:
import { main } from './';
import sinon, { SinonFakeTimers } from 'sinon';
describe('60617715', () => {
let clock: SinonFakeTimers;
beforeEach(() => {
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
it('should pass', async () => {
await main(5000, clock);
});
});
unit test results:
60617715
main
✓ should pass
1 passing (9ms)

Jest mock timers not working as expected asynchronously; how to make this test pass?

I am trying to test a queuing component that makes calls and handles a lot of scheduling. I want to test it with a mock api where the api responses are delayed as they would be in real life, but I want to use mock timers and fake the passage of time. In the following bare-bones example, the object under test is the Caller object.
function mockCall(): Promise<string> {
return new Promise<string>(resolve => setTimeout(() => resolve("success"), 20));
}
const callReceiver = jest.fn((result: string) => { console.log(result)});
class Caller {
constructor(call: () => Promise<string>,
receiver: (result: string) => void) {
call().then(receiver);
}
}
it("advances mock timers correctly", () => {
jest.useFakeTimers();
new Caller(mockCall, callReceiver);
jest.advanceTimersByTime(50);
expect(callReceiver).toHaveBeenCalled();
});
I would think this test should pass, but instead the expect is evaluated before the timer is advanced, so the test fails. How can I write this test so it will pass?
By the way, this test does pass if I use real timers and delay the expect for more than 20 milliseconds, but I am specifically interested in using fake timers and advancing time with code, not waiting for real time to elapse.
The reason is mockCall still returns Promise, even after you mocked timer. So call().then() will be executed as next microtask. To advance execution you can wrap your expect in microtask too:
it("advances mock timers correctly", () => {
jest.useFakeTimers();
new Caller(mockCall, callReceiver);
jest.advanceTimersByTime(50);
return Promise.resolve().then(() => {
expect(callReceiver).toHaveBeenCalled()
});
});
Beware of returning this Promise so jest would wait until it's done. To me using async/await it would look even better:
it("advances mock timers correctly", async () => {
jest.useFakeTimers();
new Caller(mockCall, callReceiver);
jest.advanceTimersByTime(50);
await Promise.resolve();
expect(callReceiver).toHaveBeenCalled();
});
Btw the same thing each time you mock something that is returning Promise(e.g. fetch) - you will need to advance microtasks queue as well as you do with fake timers.
More on microtasks/macrotasks queue: https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f
Jest repo has open proposal on handling pending Promises in more clear way https://github.com/facebook/jest/issues/2157 but no ETA so far.
You can make the test work by returning the promise to jest as otherwise the execution of your test method is already finished and does not wait for the promise to be fulfilled.
function mockCall() {
return new Promise(resolve => setTimeout(() => resolve('success'), 20));
}
const callReceiver = jest.fn((result) => { console.log(result); });
class Caller {
constructor(callee, receiver) {
this.callee = callee;
this.receiver = receiver;
}
execute() {
return this.callee().then(this.receiver);
}
}
describe('my test suite', () => {
it('advances mock timers correctly', () => {
jest.useFakeTimers();
const caller = new Caller(mockCall, callReceiver);
const promise = caller.execute();
jest.advanceTimersByTime(50);
return promise.then(() => {
expect(callReceiver).toHaveBeenCalled();
});
});
});

Jest setTimeout not pausing test

it('has working hooks', async () => {
setTimeout(() => {
console.log("Why don't I run?")
expect(true).toBe(true)
}, 15000)
I've already reviewed this answer, Jest documentation, and several GitHub threads:
Disable Jest setTimeout mock
Right now, the function inside the timeout doesn't run.
How can I get Jest to pause its execution of the test for 15 seconds and then run the inner function?
Thanks!
it('has working hooks', async () => {
await new Promise(res => setTimeout(() => {
console.log("Why don't I run?")
expect(true).toBe(true)
res()
}, 15000))
})
or
it('has working hooks', done => {
setTimeout(() => {
console.log("Why don't I run?")
expect(true).toBe(true)
done()
}, 15000)
})
A nice and clean way to do it (without callbacks) we can simply run an await and pass the res to setTimeout(res, XXX) like so:
it('works with await Promise and setTimeout', async () => {
// await 15000ms before continuing further
await new Promise(res => setTimeout(res, 15000));
// run your test
expect(true).toBe(true)
});
setTimeout is now available through the jest object, and it will function as you expect: https://jestjs.io/docs/jest-object#misc.
it('works with jest.setTimeout', async () => {
// pause test example for 15 seconds
jest.setTimeout(15000)
// run your test
expect(true).toBe(true)
});
Note: If you attempt to set a timeout for 5000ms or more without the jest object, jest will error and let you know to use jest.setTimeout instead.

When testing async functions with Mocha/Chai, a failure to match an expectation always results in a timeout

For example, I have something basic like this:
it.only('tests something', (done) => {
const result = store.dispatch(fetchSomething());
result.then((data) => {
const shouldBe = 'hello';
const current = store.something;
expect(current).to.equal(shouldBe);
done();
}
});
When current does not match shouldBe, instead of a message saying that they don't match, I get the generic timeout message:
Error: timeout of 2000ms exceeded. Ensure the done() callback is being
called in this test.
It's as if the expectation is pausing the script or something. How do I fix this? This is making debugging nearly impossible.
The expectation is not pausing the script, it is throwing an exception before you hit the done callback, but since it is no longer inside of the test method's context it won't get picked up by the test suite either, so you are never completing the test. Then your test just spins until the timeout is reached.
You need to capture the exception at some point, either in the callback or in the Promise's error handler.
it.only('tests something', (done) => {
const result = store.dispatch(fetchSomething());
result.then((data) => {
const shouldBe = 'hello';
const current = store.getState().get('something');
try {
expect(current).to.equal(shouldBe);
done();
} catch (e) {
done(e);
}
});
});
OR
it.only('tests something', (done) => {
const result = store.dispatch(fetchSomething());
result.then((data) => {
const shouldBe = 'hello';
const current = store.getState().get('something');
expect(current).to.equal(shouldBe);
})
.catch(done);
});
Edit
If you are not opposed to bringing in another library, there is a fairly nice library call chai-as-promised. That gives you some nice utilities for this kind of testing.

Categories

Resources