Jest 不适用于 util.promisify(setTimeout)

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 上工作,所以即使我重新实现我的应用程序中的延迟(我不想这样做),我仍然不得不忍受无法加速的缓慢测试.

这些问题中的任何一个是预期行为吗?如果不是我做错了什么?

Bri*_*ams 5

这是个有趣的问题。它一直到核心内置函数的实现。


为什么测试 5 通过而不测试 1

这花了一段时间才追上来。

默认的测试环境Jestjsdomjsdom提供它自己的实现setTimeout

调用promisify(setTimeout)jsdom测试环境中返回由运行时创建函数的代码setTimeout提供jsdom

相反,如果Jestnode测试环境中运行,调用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)

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

每隔一段时间调用 jest.runAllTimers()

事实证明,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完成。

捕获 setInterval 和 clearInterval

最后一点要注意的是,jest.useFakeTimers 替换全局定时器函数,包括setIntervalclearInterval,以便设置我们的间隔并清除它,我们需要在调用之前捕获真正的函数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)