笑话不适用于util.promisify(setTimeout)

时间:2018-10-09 18:27:14

标签: javascript node.js jestjs es6-promise

我知道在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上运行,所以即使我在我的应用中重新实现了延迟(我不想这样做),我仍然不得不经历缓慢的测试,而这些测试不能加快速度。

这两个问题中是否有一个预期的行为?如果不是,那我做错了什么?

1 个答案:

答案 0 :(得分:1)

这是一个有趣的问题。一直到核心内置功能的实现。


为什么测试5通过但未测试1

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

default test environment in Jest is jsdomjsdom提供了its own implementation for setTimeout

promisify(setTimeout)测试环境中调用jsdom返回通过在setTimeout提供的jsdom上运行this code创建的函数。

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

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.runAllTimers()

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

捕获setInterval和clearInterval

要注意的最后一点是await timeOut(10000); replaces the global timer functions包括request.getjest.useFakeTimers,因此为了设置间隔并清除间隔,我们需要捕获实际函数在致电setInterval之前。


考虑到所有这些,下面是上面列出的app.js代码的工作测试:

clearInterval