rai*_*sey 4 javascript node.js es6-promise jestjs
我知道关于 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;
});
Run Code Online (Sandbox Code Playgroud)
单独运行测试表明只有测试 5 通过。我的第一个问题是为什么测试 5 通过而不是测试 1,考虑到它们是完全相同的测试,除了基于承诺的延迟的不同实现。这两种实现都可以在 Jest 测试之外完美运行(使用 Supertest 进行测试,没有 Jest)。
虽然测试 5 确实通过了,但它使用了实时计时器,因此并不理想。就我所见,测试 6 应该是等效的假计时器(我还尝试了一个在then主体内部调用 done() 的版本),但这也失败了。
我的 Web 应用程序有一个带有处理程序的路由,util.promisify(setTimeout)因此即使使用实时计时器,Jest 也试图测试它,这使得该框架对我来说不太有用。考虑到自定义实现(测试 5)实际上确实有效,这似乎是一个错误。
尽管如此,Jest 仍然不能使用模拟计时器在测试 6 上工作,所以即使我重新实现我的应用程序中的延迟(我不想这样做),我仍然不得不忍受无法加速的缓慢测试.
这些问题中的任何一个是预期行为吗?如果不是我做错了什么?
这是个有趣的问题。它一直到核心内置函数的实现。
这花了一段时间才追上来。
在默认的测试环境Jest是jsdom和jsdom提供它自己的实现setTimeout。
调用promisify(setTimeout)在jsdom测试环境中返回由运行时创建函数的代码上setTimeout提供jsdom。
相反,如果Jest在node测试环境中运行,调用promisify(setTimeout)只会返回内置node实现。
这个简单的测试在node测试环境中通过,但挂在jsdom:
const { promisify } = require('util');
test('promisify(setTimeout)', () => {
return promisify(setTimeout)(0).then(() => {
expect(true).toBe(true);
});
});
Run Code Online (Sandbox Code Playgroud)
结论:在promisify的-ed版本setTimeout提供jsdom不起作用。
如果在node测试环境中运行,则测试 1 和测试 5 均通过
promisify(setTimeout)与 Timer Mocks 一起使用的测试代码听起来真正的问题是如何使用Timer Mocks测试这样的代码:
应用程序.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;
Run Code Online (Sandbox Code Playgroud)
这花了一段时间才弄明白,我将逐一介绍每个部分。
promisify(setTimeout)无法在不promisify(setTimeout)模拟的情况下测试使用 Timer Mocks 的代码promisify(setTimeout):
jsdom环境中promisify(setTimeout)会挂。node环境promisify(setTimeout)中将提供node不调用的实现,setTimeout因此在jest.useFakeTimers替换全局时setTimeout它不会产生任何影响。promisify(setTimeout) 可以通过创建以下内容来模拟__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;
Run Code Online (Sandbox Code Playgroud)
请注意,由于是核心 Node 模块,因此需要调用jest.mock('util');测试。util
事实证明,request.get启动了一个supertest使用JavaScript 事件循环的整个过程,并且在当前运行的消息(测试)完成之前不会运行任何东西。
这是有问题的,因为request.get最终会运行app.get,然后调用await timeOut(10000);它直到jest.runAllTimers被调用才会完成。
同步测试中的任何内容都将在 request.get执行任何操作之前运行,因此如果jest.runAllTimers在测试期间运行,则不会对以后调用await timeOut(10000);.
此问题的解决方法是设置一个时间间隔,以定期将调用jest.runAllTimers. 当调用await timeOut(10000);运行的消息将在该行上暂停时,jest.runAllTimers将运行消息调用,然后等待的消息await timeOut(10000);将能够继续并request.get完成。
最后一点要注意的是,jest.useFakeTimers 替换全局定时器函数,包括setInterval等clearInterval,以便设置我们的间隔并清除它,我们需要在调用之前捕获真正的函数jest.useFakeTimers。
考虑到所有这些,下面是对上面列出的 app.js 代码的工作测试:
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
});
});
Run Code Online (Sandbox Code Playgroud)