【问题标题】:Jest doesn't work with util.promisify(setTimeout)玩笑不适用于 util.promisify(setTimeout)
【发布时间】:2019-03-14 14:20:30
【问题描述】:

我知道关于 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 测试之外完美运行(使用 Supertest 没有 Jest 进行测试)。

虽然测试 5 确实通过了,但它使用的是实时计时器,因此并不理想。就我所见,测试 6 应该是等效的假计时器(我还尝试了在 then 正文中调用 done() 的版本),但这也失败了。

我的 web 应用程序有一个带有使用 util.promisify(setTimeout) 的处理程序的路由,因此 Jest 试图测试它的事实,即使使用真正的计时器,也使得该框架对我的用处大大降低。考虑到自定义实现(测试 5)确实有效,这似乎是一个错误。

尽管如此,Jest 仍然无法使用模拟计时器进行测试 6,因此即使我在我的应用程序中重新实现延迟(我不想这样做),我仍然不得不忍受无法进行的缓慢测试加快速度。

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

【问题讨论】:

  • "为什么测试 5 通过而不是测试 1,因为它们是完全相同的测试" - 测试 5 使用 async 函数并且确实返回了一个对开玩笑的承诺,与测试 3 相比,与测试 1 更相似。
  • 是的,Jest 不能模拟一个承诺的 setTimeout 听起来像是一个错误。还是我们应该称其为promisify 的一个功能?
  • @skyboyer 你确定吗?异步函数返回没有返回关键字的承诺,不是吗?请注意,当 Jest 文档中的示例具有异步功能时,它们都没有使用 return
  • @Bergi 那是我的错误。我在我的编辑器中更新了它,但没有将更改复制到这里。 1 和 5 现在完全等价了
  • @skyboyer 实际上,如果您查看 docs 的 setTimeout,您会看到直接引用了 promisified 版本并给出了示例。

标签: javascript node.js jestjs es6-promise


【解决方案1】:

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


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

这需要一段时间才能追上。

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

jsdom 测试环境中调用promisify(setTimeout) 会返回在jsdom 提供的setTimeout 上运行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);
    });
});

结论jsdom提供的setTimeoutpromisify-ed版本不起作用。

如果在node 测试环境中运行,测试 1 和测试 5 均通过


使用 promisify(setTimeout) 和 Timer Mocks 的测试代码

听起来真正的问题是如何使用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;

这需要一段时间才能弄清楚,我将逐步介绍每个部分。

模拟promisify(setTimeout)

如果不模拟 promisify(setTimeout),则无法使用 Timer Mocks 测试使用 promisify(setTimeout) 的代码:

promisify(setTimeout) can be mocked 通过创建以下__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;

注意在测试中调用jest.mock('util'); is required since util is a core Node module

间隔调用 jest.runAllTimers()

事实证明,request.getsupertest 中启动了一个使用JavaScript Event Loop 的整个过程,并且在当前运行的消息(测试)完成之前不会运行任何东西。

这是有问题的,因为request.get 最终会运行app.get,然后会调用await timeOut(10000);,直到调用jest.runAllTimers 才会完成。

同步测试中的任何内容都将运行之前 request.get 执行任何操作,因此如果在测试期间运行jest.runAllTimers,它不会对稍后对await timeOut(10000); 的调用产生任何影响。

解决此问题的方法是设置一个间隔,定期在 JavaScript 事件循环中对调用 jest.runAllTimers 的消息进行排队。当调用await timeOut(10000); 的消息运行时,它将在该行暂停,然后调用jest.runAllTimers 的消息将运行,等待await timeOut(10000); 的消息将能够继续,request.get 将完成。

捕获 setInterval 和 clearInterval

最后要注意的是jest.useFakeTimersreplaces the global timer functions包括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
  });
});

【讨论】:

  • 非常感谢!来自无关的东西,但 Jest 在使用 jest.useFakeTimers() 后挂起我的测试。我不知道如何撤消 jest.useFakeTimers()jest.useRealTimers() 工作!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-24
  • 2019-07-08
  • 1970-01-01
  • 2015-07-15
  • 1970-01-01
  • 2019-09-11
  • 2018-02-15
相关资源
最近更新 更多