我知道在SO上有很多类似的问题,但是我相信我的与众不同,当前的任何答案都无法回答。
我正在Express.JS中测试REST API。下面是一个最小的工作示例和几个不同编号的测试用例。
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;
});
生动地运行测试表明只有5个测试通过。 然后,我的第一个问题是,为什么测试5通过而不是测试1,考虑到它们是完全相同的测试,而不是基于承诺的延迟的不同实现。 两种实现都可以在Jest测试之外(使用不含Jest的Supertest进行测试)完美地工作。
虽然测试5确实通过了,但它使用了真正的计时器,因此并不理想。就我所知,测试6应该是等效的伪计时器(我也尝试过在then
主体中调用了done()的版本),但这同样失败。
我的Web应用程序具有一个使用util.promisify(setTimeout)
的处理程序的路由,因此Jest试图测试它(即使使用真正的计时器)也无济于事,这使该框架对我的用处不大。考虑到自定义实现(测试5)确实有效,这似乎是一个错误。
尽管如此,Jest仍然无法在带有模拟计时器的测试6上运行,所以即使我在我的应用中重新实现了延迟(我不想这样做),我仍然不得不经历缓慢的测试,而这些测试不能加快速度。
这两个问题中是否有一个预期的行为?如果不是,那我做错了什么?
答案 0 :(得分:1)
这是一个有趣的问题。一直到核心内置功能的实现。
这花了一段时间才能追上。
default test environment in Jest
is jsdom
和jsdom
提供了its own implementation for setTimeout
。
在promisify(setTimeout)
测试环境中调用jsdom
返回通过在setTimeout
提供的jsdom
上运行this code创建的函数。
相反,如果Jest
在node
测试环境中运行,则调用promisify(setTimeout)
只会返回built-in node
implementation。
此简单测试在node
测试环境中通过,但在jsdom
处挂起:
const { promisify } = require('util');
test('promisify(setTimeout)', () => {
return promisify(setTimeout)(0).then(() => {
expect(true).toBe(true);
});
});
结论: promisify
提供的setTimeout
的{{1}}版的版本不起作用。
如果在jsdom
测试环境中运行,则测试1和测试5都会通过
node
和Timer Mocks的测试代码听起来真正的问题是如何使用Timer Mocks测试这样的代码:
app.js
promisify(setTimeout)
花了一段时间才能弄清楚,我将遍历每个部分。
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;
在不模拟promisify(setTimeout)
的情况下,无法使用计时器模拟来测试使用promisify(setTimeout)
的代码:
promisify(setTimeout)
环境中,jsdom
将挂起。promisify(setTimeout)
环境中,node
将是the implementation provided by node
,它不会调用promisify(setTimeout)
,因此,当jest.useFakeTimers
replaces the global setTimeout
时它不会起作用。 setTimeout
can be mocked通过创建以下promisify(setTimeout)
:
__mocks__/util.js
请注意,在测试is required since util
is a core Node module中调用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;
。
事实证明,jest.mock('util');
在request.get
中启动了使用JavaScript Event Loop的整个过程,直到当前运行的消息(测试)完成后才运行任何程序。
这是有问题的,因为supertest
最终将运行request.get
,然后将调用app.get
,直到调用await timeOut(10000);
时,jest.runAllTimers
才会完成。
同步测试中的任何内容都将在之前运行。request.get
会执行任何操作,因此,如果jest.runAllTimers
在测试期间运行,则不会对以后对{的调用产生任何影响{1}}。
此问题的解决方法是设置一个时间间隔,该时间间隔定期在JavaScript事件循环中将调用await timeOut(10000);
的消息排队。当调用jest.runAllTimers
的消息运行时,它将在该行上暂停,然后将运行一条调用await timeOut(10000);
的消息,等待jest.runAllTimers
的消息将能够继续执行,并且{{1 }}将完成。
要注意的最后一点是await timeOut(10000);
replaces the global timer functions包括request.get
和jest.useFakeTimers
,因此为了设置间隔并清除间隔,我们需要捕获实际函数在致电setInterval
之前。
考虑到所有这些,下面是上面列出的app.js代码的工作测试:
clearInterval