admin管理员组文章数量:1336632
I am aware there are a lot of similar questions on SO, but I believe mine is different and not answered by any of the current answers.
I am testing a REST API in Express.JS. Below is a minimal working example and several different numbered test cases.
const express = require("express");
let request = require("supertest");
const { promisify } = require("util");
const app = express();
request = request(app);
const timeOut = promisify(setTimeout);
const timeOut2 = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
app.locals.message = "Original string";
app.get("/one", async (req, res) => {
await timeOut(1000);
res.send(app.locals.message);
});
app.get("/two", (req, res) => {
res.send(app.locals.message);
});
app.get("/three", async (req, res) => {
await timeOut2(1000);
res.send(app.locals.message);
});
test("1. test promisify", async () => {
expect.assertions(1);
const response = await request.get("/one");
expect(response.text).toEqual("Original string");
});
test("2. test promisify with fake timers", () => {
expect.assertions(1);
jest.useFakeTimers();
request.get("/one").then(res => {
expect(res.text).toEqual("Original string");
});
jest.runAllTimers();
});
test("3. test promisify with fake timers and returning pending promise", () => {
expect.assertions(1);
jest.useFakeTimers();
const response = request.get("/one").then(res => {
expect(res.text).toEqual("Original string");
});
jest.runAllTimers();
return response;
});
test("4. test no timeout", async () => {
expect.assertions(1);
const response = await request.get("/two");
expect(response.text).toEqual("Original string");
});
test("5. test custom timeout", async () => {
expect.assertions(1);
const response = await request.get("/three");
expect(response.text).toEqual("Original string");
});
test("6. test custom timeout with fake timers", () => {
expect.assertions(1);
jest.useFakeTimers();
const response = request.get("/three").then(res => {
expect(res.text).toEqual("Original string");
});
jest.runAllTimers();
return response;
});
Running the tests invividually shows that only test 5 passes. My first question then is why does test 5 pass and not test 1, considering they are exactly the same test, other than a different implementation of the promise-based delay. Both implementations work perfectly outside of Jest tests (tested using Supertest without Jest).
While test 5 does pass, it uses real timers so is not ideal. Test 6 should be the fake timer equivalent as far as I can see (I also tried a version with done() called inside the then
body), yet this fails as well.
My web app has a route with a handler that uses util.promisify(setTimeout)
so the fact that Jest falls on its face trying to test it, even with real timers, makes the framework much less useful to me. This seems to be a bug considering that a custom implementation (test 5) actually does work.
Nonetheless, Jest still doesn't work on test 6 with mock timers so even if I reimplement the delays in my app (which I don't want to do), I would still have to suffer slow tests that can't be sped up.
Are either of these issues expected behaviour? If not what am I doing wrong?
I am aware there are a lot of similar questions on SO, but I believe mine is different and not answered by any of the current answers.
I am testing a REST API in Express.JS. Below is a minimal working example and several different numbered test cases.
const express = require("express");
let request = require("supertest");
const { promisify } = require("util");
const app = express();
request = request(app);
const timeOut = promisify(setTimeout);
const timeOut2 = time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
app.locals.message = "Original string";
app.get("/one", async (req, res) => {
await timeOut(1000);
res.send(app.locals.message);
});
app.get("/two", (req, res) => {
res.send(app.locals.message);
});
app.get("/three", async (req, res) => {
await timeOut2(1000);
res.send(app.locals.message);
});
test("1. test promisify", async () => {
expect.assertions(1);
const response = await request.get("/one");
expect(response.text).toEqual("Original string");
});
test("2. test promisify with fake timers", () => {
expect.assertions(1);
jest.useFakeTimers();
request.get("/one").then(res => {
expect(res.text).toEqual("Original string");
});
jest.runAllTimers();
});
test("3. test promisify with fake timers and returning pending promise", () => {
expect.assertions(1);
jest.useFakeTimers();
const response = request.get("/one").then(res => {
expect(res.text).toEqual("Original string");
});
jest.runAllTimers();
return response;
});
test("4. test no timeout", async () => {
expect.assertions(1);
const response = await request.get("/two");
expect(response.text).toEqual("Original string");
});
test("5. test custom timeout", async () => {
expect.assertions(1);
const response = await request.get("/three");
expect(response.text).toEqual("Original string");
});
test("6. test custom timeout with fake timers", () => {
expect.assertions(1);
jest.useFakeTimers();
const response = request.get("/three").then(res => {
expect(res.text).toEqual("Original string");
});
jest.runAllTimers();
return response;
});
Running the tests invividually shows that only test 5 passes. My first question then is why does test 5 pass and not test 1, considering they are exactly the same test, other than a different implementation of the promise-based delay. Both implementations work perfectly outside of Jest tests (tested using Supertest without Jest).
While test 5 does pass, it uses real timers so is not ideal. Test 6 should be the fake timer equivalent as far as I can see (I also tried a version with done() called inside the then
body), yet this fails as well.
My web app has a route with a handler that uses util.promisify(setTimeout)
so the fact that Jest falls on its face trying to test it, even with real timers, makes the framework much less useful to me. This seems to be a bug considering that a custom implementation (test 5) actually does work.
Nonetheless, Jest still doesn't work on test 6 with mock timers so even if I reimplement the delays in my app (which I don't want to do), I would still have to suffer slow tests that can't be sped up.
Are either of these issues expected behaviour? If not what am I doing wrong?
Share Improve this question edited Oct 9, 2018 at 19:25 Bergi 666k161 gold badges1k silver badges1.5k bronze badges asked Oct 9, 2018 at 18:27 raikseyraiksey 3413 silver badges10 bronze badges 7-
1
"why does test 5 pass and not test 1, considering they are exactly the same test" - test 5 uses an
async
function and does return a promise to jest, it is more similar to test 3 than to test 1. – Bergi Commented Oct 9, 2018 at 18:36 -
Yes, Jest not being able to mock a promisified
setTimeout
sounds like a bug. Or should we call it a feature ofpromisify
? – Bergi Commented Oct 9, 2018 at 18:37 -
@skyboyer Are you sure? async functions return promises without a return keyword, do they not? Note that none of the examples in the Jest docs use
return
when they have an async function. – raiksey Commented Oct 9, 2018 at 19:08 - @Bergi That's my mistake. I updated it in my editor but didn't copy the changes over here. 1 and 5 are now pletely equivalent – raiksey Commented Oct 9, 2018 at 19:10
- 1 @skyboyer In fact if you look at the docs for setTimeout, you see that the promisified version is directly referred to and example given. – raiksey Commented Oct 10, 2018 at 1:28
1 Answer
Reset to default 7This is an interesting question. It gets all the way down to the implementation of core built-in functions.
Why does test 5 pass and not test 1
This took a while to chase down.
The default test environment in Jest
is jsdom
and jsdom
provides its own implementation for setTimeout
.
Calling promisify(setTimeout)
in the jsdom
test environment returns the function created by running this code on the setTimeout
provided by jsdom
.
In contrast, if Jest
is running in the node
test environment calling promisify(setTimeout)
simply returns the built-in node
implementation.
This simple test passes in the node
test environment, but hangs in jsdom
:
const { promisify } = require('util');
test('promisify(setTimeout)', () => {
return promisify(setTimeout)(0).then(() => {
expect(true).toBe(true);
});
});
Conclusion: The promisify
-ed version of the setTimeout
provided by jsdom
doesn't work.
Test 1 and test 5 both pass if run in the node
test environment
Test code that uses promisify(setTimeout)
with Timer Mocks
It sounds like the real question is how to test code like this with Timer Mocks:
app.js
const express = require("express");
const { promisify } = require("util");
const app = express();
const timeOut = promisify(setTimeout);
app.locals.message = "Original string";
app.get("/one", async (req, res) => {
await timeOut(10000); // wait 10 seconds
res.send(app.locals.message);
});
export default app;
This took a while to figure out, I'll walk through each part.
Mock promisify(setTimeout)
It is not possible to test code that uses promisify(setTimeout)
using Timer Mocks without mocking promisify(setTimeout)
:
- In the
jsdom
environmentpromisify(setTimeout)
will hang. - In the
node
environmentpromisify(setTimeout)
will be the implementation provided bynode
which doesn't callsetTimeout
so whenjest.useFakeTimers
replaces the globalsetTimeout
it won't have any effect.
promisify(setTimeout)
can be mocked by creating the following __mocks__/util.js
:
const util = require.requireActual('util'); // get the real util
const realPromisify = util.promisify; // capture the real promisify
util.promisify = (...args) => {
if (args[0] === setTimeout) { // return a mock if promisify(setTimeout)
return time =>
new Promise(resolve => {
setTimeout(resolve, time);
});
}
return realPromisify(...args); // ...otherwise call the real promisify
}
module.exports = util;
Note that calling jest.mock('util');
in the test is required since util
is a core Node module.
Call jest.runAllTimers() on an interval
As it turns out, request.get
kicks off a whole process in supertest
that uses the JavaScript Event Loop and doesn't run anything until the current running message (the test) has pleted.
This is problematic since request.get
will eventually run app.get
which will then call await timeOut(10000);
which won't plete until jest.runAllTimers
has been called.
Anything in the synchronous test will run before request.get
does anything so if jest.runAllTimers
is run during the test it won't have any effect on the later call to await timeOut(10000);
.
The workaround for this issue is to set up an interval that periodically queues messages in the JavaScript Event Loop that call jest.runAllTimers
. When the message that calls await timeOut(10000);
runs it will pause on that line, a message calling jest.runAllTimers
will then run, and the message waiting for await timeOut(10000);
will then be able to proceed and request.get
will plete.
Capture setInterval and clearInterval
The last point to note is that jest.useFakeTimers
replaces the global timer functions including setInterval
and clearInterval
so in order to set up our interval and clear it we need to capture the real functions before calling jest.useFakeTimers
.
With all that in mind, here is a working test for the app.js code listed above:
jest.mock('util'); // core Node.js modules must be explicitly mocked
const supertest = require('supertest');
import app from './app';
const request = supertest(app);
const realSetInterval = setInterval; // capture the real setInterval
const realClearInterval = clearInterval; // capture the real clearInterval
beforeEach(() => {
jest.useFakeTimers(); // use fake timers
});
afterEach(() => {
jest.useRealTimers(); // restore real timers
});
test("test promisify(setTimeout) with fake timers", async () => {
expect.assertions(1);
const interval = realSetInterval(() => {
jest.runAllTimers(); // run all timers every 10ms
}, 10);
await request.get("/one").then(res => {
realClearInterval(interval); // cancel the interval
expect(res.text).toEqual("Original string"); // SUCCESS
});
});
本文标签: javascriptJest doesn39t work with utilpromisify(setTimeout)Stack Overflow
版权声明:本文标题:javascript - Jest doesn't work with util.promisify(setTimeout) - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1742414345a2470418.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论