Jest не работает с util.promisify (setTimeout) - PullRequest
0 голосов
/ 09 октября 2018

Я знаю, что есть много похожих вопросов по SO, но я считаю, что у меня другие вопросы, и на них нет ответов ни на один из текущих ответов.

Я тестирую REST API в Express.JS.Ниже приведен минимальный рабочий пример и несколько различных пронумерованных тестовых случаев.

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 должен быть поддельным таймером (я также пробовал версию с функцией done (), вызываемой внутри тела then), но это тоже не удается.

Мое веб-приложение имеетroute с обработчиком, который использует util.promisify(setTimeout), поэтому тот факт, что Jest падает, пытаясь протестировать его, даже с реальными таймерами, делает среду намного менее полезной для меня.Это кажется ошибкой, учитывая, что пользовательская реализация (тест 5) действительно работает.

Тем не менее, Jest по-прежнему не работает в тесте 6 с таймерами, поэтому даже если я переопределю задержки в моем приложении (что я не хочу делать), мне все равно придется терпеть медленные тесты, которые нельзя ускорить.

Ожидается ли поведение этих проблем?Если нет, то что я делаю не так?

1 Ответ

0 голосов
/ 12 октября 2018

Это интересный вопрос.Все начинается с реализации встроенных в ядро ​​функций.


Почему тест 5 проходит, а не тест 1

Потребовалось некоторое время, чтобы отследить.

Средой тестирования по умолчанию в 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);
    });
});

Заключение : * * * * * * * * * * * * * setTimeout -обработанная jsdom версия *1043* не работает.

Тест 1 и тест 5 оба проходят, если они выполняются в node тестовой среде


Тестовый код, который использует promisify(setTimeout) с таймерами 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), используя Таймерные макеты без насмешки 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;

Обратите внимание, что вызов jest.mock('util'); в тесте требуется, так как util является базовым модулем Node .

Call jest.runAllTimers () в интервале

Как оказалось, request.get запускает целый процесс в supertest, который использует JavaScript Event Loop и ничего не запускает до текущегорunning message (тест) завершен.

Это проблематично, поскольку 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 завершится.

Capture setInterval и clearInterval

Последнее замечание: 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
  });
});
...