Jest: 提供 API 来刷新 Promise 解析队列

创建于 2016-11-23  ·  46评论  ·  资料来源: facebook/jest

您要请求功能还是报告错误

_Feature_,我猜,但在测试使用Promise的代码时非常重要。

目前的行为是什么?

我有一个组件,它在外部异步操作的后续操作中使用Promise在内部进行包装和链接。 我正在提供异步操作的模拟并解决它在我的测试中返回的承诺。

组件是这样的:

class Component extends React.Component {
  // ...
  load() {
    Promise.resolve(this.props.load())
      .then(
        result => result
          ? result
          : Promise.reject(/* ... */)
        () => Promise.reject(/* ... */)
      )
      .then(result => this.props.afterLoad(result));
  }
}

测试代码如下所示:

const load = jest.fn(() => new Promise(succeed => load.succeed = succeed));
const afterLoad = jest.fn();
const result = 'mock result';
mount(<Component load={load} afterLoad={afterLoad} />);
// ... some interaction that requires the `load`
load.succeed(result);
expect(afterLoad).toHaveBeenCalledWith(result);

测试失败,因为expect()在链接的承诺处理程序之前被评估。 我必须在测试中复制内部承诺链的长度才能得到我需要的东西,如下所示:

return Promise.resolve(load.succeed(result))
  // length of the `.then()` chain needs to be at least as long as in the tested code
  .then(() => {})
  .then(() => expect(result).toHaveBeenCalledWith(result));

什么是预期行为?

我希望 Jest 提供某种 API 来刷新所有挂起的承诺处理程序,例如:

load.succeed(result);
jest.flushAllPromises();
expect(result).toHaveBeenCalledWith(result);

我试过runAllTicksrunAllTimers没有效果。


_或者,如果我只是错过了一些已经存在的功能或模式,我希望这里有人能指出我正确的方向:)_

Enhancement New API proposal

最有用的评论

辅助函数可以将其转换为承诺本身,因此您无需处理 done 回调。 它足够小,放在用户区是无害的,但如果把它放在开玩笑的对象上,我不会抱怨。 像这样的东西在我的项目中得到了很多使用。

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

test('', () => {
  somethingThatKicksOffPromiseChain();
  return flushPromises().then(() => {
    expect(...
  });
})

使用 async await 它几乎很漂亮:

test('', async () => {
  somethingThatKicksOffPromiseChain();
  await flushPromises();
  expect(...
})

所有46条评论

虽然异步测试承诺是好的,但记住你可以将测试函数作为 Promise 返回,所以这样的事情会起作用:

test('my promise test', () => { //a test function returning a Promise
  return Promise.resolve(load.succeed(result))
    .then(() => {})
    .then(() => expect(result).toHaveBeenCalledWith(result));
})

从测试函数返回一个 Promise 使 Jest 意识到这是一个异步测试并等待它被解决或超时。

@thymikee当然,我返回值是为了让 Jest 等待——这完全不是重点。 注意你是如何在代码中留下.then(() => {})的。 我不知道如何比在开篇文章中更简洁地描述问题。 请仔细阅读并重新打开问题或描述如何解决它。

_我已将return到 OP 中的代码中以避免混淆。_

遇到了类似的问题,并在此处进行了描述: https :

简而言之:我试图断言由于用户交互(使用酶模拟)而分派到 Redux 存储的操作序列。 使用 Promises 调度同步和异步的操作(模拟以立即解决)。 如果没有直接访问 Promise 链的权限,那么在 Promise 链用完之后似乎就没有办法断言了。 setTimeout(..., 0)可以工作,但感觉很糟糕,如果setTimeout回调中的断言失败,Jest 会因超时错误(而不是断言错误)而失败。

flushAllPromises的想法似乎是一个解决方案,虽然我认为这就是runAllTicks应该做的?

作为后续:我尝试用setTimeout(..., 0) setImmediate替换setTimeout(..., 0) ,这似乎在 Promise 回调微任务队列耗尽后运行断言,并防止 Jest 在断言错误时超时。 因此,这可以正常工作,并且是我的用例可接受的解决方案:

test('changing the reddit downloads posts', done => {
    setImmediate(() => {
        // assertions...
        done()
    })
})

辅助函数可以将其转换为承诺本身,因此您无需处理 done 回调。 它足够小,放在用户区是无害的,但如果把它放在开玩笑的对象上,我不会抱怨。 像这样的东西在我的项目中得到了很多使用。

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

test('', () => {
  somethingThatKicksOffPromiseChain();
  return flushPromises().then(() => {
    expect(...
  });
})

使用 async await 它几乎很漂亮:

test('', async () => {
  somethingThatKicksOffPromiseChain();
  await flushPromises();
  expect(...
})

@jwbay那里有一些不错的糖🍠!

这个flushPromises确实是一行一行,但是如何到达那一行却一点也不明显。 因此,我认为将 Jest 用户作为 util 函数使用是有益的。

@pekala one liner IMO 不提供所需的行为,因为它不会等到以下未决承诺得到解决:

function foo() {  
  return new Promise((res) => {
    setTimeout(() => {
      res()
    }, 2000);
  });
}

swizzling Promise 怎么样?当创建一个新的 Promise 时,将它添加到某个数组中,然后刷新所有 Promise 将在 Promise.all 上等待这个数组?

@talkol我认为它会的,只要你也是我们的假计时器。 不过我还没有测试过。

@pekala无需在此示例中伪造计时器,因为承诺仅在达到时间后才会解决
我只是担心混杂的 Promise 会干扰玩笑的内部运作,这有点硬核

如果您不伪造计时器,您的测试将需要真正的 2s+ 才能完成。 我认为最好的做法是消除这些类型的延迟,在这种情况下, @jwbay提出的flushPromises可以完成这项工作。

一切都取决于您要测试的内容:) 我要说的是计时器与等待承诺无关

我们正在解决与 Promises 未解决的相关问题,这些问题与 setTimeout 调用混合在一起。 在 jest v19.0.2 中我们没有问题,但是在 jest v20.0.0 中 Promises 从不进入 resolve/reject 函数,因此测试失败。 我们的问题似乎与没有 _an API 来刷新 Promise 解析队列的问题有关,但是这个问题似乎早于我们开始看到问题的 jest v20.0.0,所以我不完全确定。

这是唯一的解决方案,我们已经能够先到了一些我们的测试,因为我们有一系列的交替setTimeout S和Promise S IN所用的代码,最终调用onUpdateFailed回调。

  ReactTestUtils.Simulate.submit(form);
  return Promise.resolve()
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => {
      expect(onUpdateFailed).toHaveBeenCalledTimes(1);
      expect(getErrorMessage(page)).toEqual('Input is invalid.');
    });

不太漂亮,所以这里的任何建议都非常感谢。

另一个不能从测试返回承诺的例子:

describe('stream from promise', () => {
  it('should wait till promise resolves', () => {
    const stream = Observable.fromPromise(Promise.resolve('foo'));
    const results = [];
    stream.subscribe(data => { results.push(data); });
    jest.runAllTimers();
    expect(results).toEqual(['foo']);
  });
});

这个测试失败了 jest 20.0.4。

@philwhln的解决方案也可以用 async/await 编写

ReactTestUtils.Simulate.submit(form);

await jest.runOnlyPendingTimers();
await jest.runOnlyPendingTimers();
await jest.runOnlyPendingTimers();

expect(onUpdateFailed).toHaveBeenCalledTimes(1);
expect(getErrorMessage(page)).toEqual('Input is invalid.');

我想要一个可以刷新承诺队列的实用函数

我也喜欢在测试之间刷新承诺队列的功能。

我正在测试使用 Promise.all 包装多个承诺的代码。 当这些包装的承诺之一失败(因为这是我想要测试的)时,承诺会立即返回,这意味着其他承诺有时(竞争条件,非确定性)在下一个测试运行时返回。

这对我的测试造成了各种破坏,具有不可预测/可重复的结果。

为了正确实现这一点,我们需要模拟Promise以便我们最终可以看到所有排队的微任务以同步解决它们。 有什么阻碍了promise-mock正在做的事情。

已经有一个 API 来刷新用process.nextTick排队的微任务,并且该 API 应该也可以与 Promises ( jest.runAllTicks ) 一起使用。

我有一个与 jasmine 挂钩的解决方案,它连接到 Yaku 的 nextTick,一个承诺库,并捕获 nextTick 调用并允许提前播放它们。
然而,jest 使用了 promises 本身,这使得这有问题。
最后,我使用了 Yaku 并对其进行了黑客攻击,以获得一个刷新它的队列的方法。 默认情况下,它使用 nextTick 正常运行,但如果您调用刷新所有挂起的承诺处理程序执行。
来源在这里:
https://github.com/lukeapage/yaku-mock
它可以用来整理、联系 ysmood 以了解他们的想法并添加文档,但它几乎可以满足您的需求,并且作为使 Promise 在测试中同步的简单解决方案对我有用。

作为一个简单的解决方法,我喜欢@jwbay的解决方案。

我们添加类似于jest对象的东西怎么样?

await jest.nextTick();

实施为

const nextTick = () => new Promise(res => process.nextTick(res));

抄送@cpojer @SimenB @rogeliog

我正在使用酶来安装 React 组件。

我也有期望 Promise 执行的函数,但上述修复都没有奏效。 我将能够在我的测试中同步处理它们 - 如果 - 函数使用await返回 Promise 对象,但不幸的是这些函数不返回 Promise 对象。

这是我最终对全局 Promise 函数使用间谍的解决方法。

global.Promise = require.requireActual('promise');

it('my test', async () => {
    const spy = sinon.spy(global, 'Promise');

    wrapper.props().dispatch(functionWithPromiseCalls());

    for (let i = 0; i < spy.callCount; i += 1) {
      const promise = spy.getCall(i);
      await promise.returnValue;
    }

    expect(...)
});

我遇到了一个用例(感谢@jwbay提供了很棒的技术)

例如,您想检查您的函数是否有超时,并且超时是精确执行的:

      jest.useFakeTimers();
      const EXPECTED_DEFAULT_TIMEOUT_MS = 10000;

      const catchHandler = jest.fn().mockImplementationOnce(err => {
        expect(err).not.toBeNull();
        expect(err.message).toContain('timeout');
      });

      // launch the async func returning a promise
      fetchStuffWithTimeout().catch(catchHandler);

      expect(catchHandler).not.toHaveBeenCalled(); // not yet

      jest.advanceTimersByTime(EXPECTED_DEFAULT_TIMEOUT_MS - 1);
      await flushPromises();

      expect(catchHandler).not.toHaveBeenCalled(); // not yet

      jest.advanceTimersByTime(1);
      await flushPromises();

      expect(catchHandler).toHaveBeenCalledTimes(1); // ok, rejected precisely

返回承诺不允许检查解决/拒绝的精确时间。

那里需要一个承诺刷新。 没有它,期望被称为太早。

希望这有助于缩小问题的范围。

对于跟随的人,这里有一个公开的 PR:#6876

https://github.com/airbnb/enzyme/issues/1587交叉发布

我想知道以下模式是否足以解决这个问题,以及我是否正在做一些被认为是不好的做法而我不应该做的事情。

人们如何看待这种方法?

export class MyComponent extends React.Component {
  constructor (props) {
    super(props)

    this.hasFinishedAsync = new Promise((resolve, reject) => {
      this.finishedAsyncResolve = resolve
    })
  }

  componentDidMount () {
    this.doSomethingAsync()
  }

  async doSomethingAsync () {
    try {
      actuallyDoAsync()
      this.props.callback()
      this.finishedAsyncResolve('success')
    } catch (error) {
      this.props.callback()
      this.finishedAsyncResolve('error')
    }
  }

  // the rest of the component
}

在测试中:

it(`should properly await for async code to finish`, () => {
  const mockCallback = jest.fn()
  const wrapper = shallow(<MyComponent callback={mockCallback}/>)

  expect(mockCallback.mock.calls.length).toBe(0)

  await wrapper.instance().hasFinishedAsync

  expect(mockCallback.mock.calls.length).toBe(1)
})

当异步调用没有在 componentDidMount 中直接完成时,我遇到了一个问题,但它正在调用一个异步函数,它正在调用另一个异步函数等等。 如果我在所有异步链中添加了一个额外的异步步骤,我需要添加一个额外的.then()或一个额外的await ,但这工作得很好。

有什么理由我不应该使用这种方法,或者这对人们来说看起来不错吗?

我在用户空间进行了一次冒险,发现它实际上是可行的,而且还不错(尽管如果您没有地图,则会遇到很多陷阱)。 这是一份体验报告,(希望)足够详细,可以直接使用; TLDR 是将async / await转译为 promise,并为 bluebird 交换本地 promise,为 lolex 交换本地计时器; 转译一切,包括node_modules/queueMicrotask是 promise 所需的原语,但默认情况下 lolex 不会提供它,因为 JSDOM 不提供它。

我在jest.mockAllTimers()和 React 组件中遇到了同样的问题,这些组件在componentDidMount()中调用了Promise componentDidMount()

#issuecomment-279171856 的解决方案以优雅的方式解决了问题。

我们需要官方 Jest API 中类似的东西!

我最近在升级一堆东西时遇到了一个问题,它在一堆测试中发现了一个问题,我们并不总是等待承诺完成。 虽然像await new Promise(resolve => setImmediate(resolve));这样的方法在简单的情况下确实有效,但我在测试中发现,我必须运行几次才能清除管道。 这就是@quasicomputational在他们的探索中提到这里。 不幸的是,我认为没有办法知道管道何时畅通,而无需在承诺创建时拦截它们。 所以我创建了一个小库来做到这一点...... promise-spy 。 尽管如此,我确实有一个使用假计时器的测试,但它并没有用它来工作......所以它还不是一个完全有效的解决方案。

虽然我也想象他们可能只在你的代码中使用async / await来测试,如果它们被转换为承诺。 如果它们没有被转换为 promises,那么这个库将无法钩入它们并等待它们完成。

我发现自己有同样的问题,我意识到:
我们不应该刷新挂起的承诺,而是如果有挂起的承诺,我们应该让整个测试失败。
这样我们将被迫使用 Abort Controller 中止测试代码中的挂起承诺:
https://developers.google.com/web/updates/2017/09/abortable-fetch
开玩笑刷新承诺等于说“并发很难,所以我们不要测试它”。 实际上,情况应该恰恰相反。
由于并发很难,我们应该更多地测试它,并且根本不允许测试通过挂起的承诺。

鉴于在这个 Stackoverflow 问题上中止承诺的混乱很明显(还)不是一件容易的事情:
https://stackoverflow.com/a/53933849/373542
我将尝试编写一个中止我的 fetch 承诺的 KISS 实现,并将在此处发布结果。

@giorgio-zamparelli:_“并发很难,所以我们不要测试它”_ 完全与原始报告的观点不符。 该问题与 _pending_ 承诺无关,而是与通过测试中的异步代码等待承诺 _resolution_ 传播的事实无关。

我认为红红火火的承诺会治愈症状而不是疾病。

Promise 应在测试中正常解析,无需刷新。
如果您的测试中有一个未决的承诺,您应该等待它使用例如@testing-library/react wait ,或者如果未决的承诺不在测试范围内,您应该要么模拟启动它的代码,或者您应该使用AbortController在诸如 React willUnmount 生命周期事件的某处中止挂起的承诺

AbortController 是一个几乎没有人使用的新 API,我感觉它是测试中大多数悬而未决的承诺的修复。

证明我是错的:
如果有人报告在此问题中遇到未决问题的问题已经尝试使用 AbortController 和 jest.mock 并且这还不够,那么我很容易被证明是错误的。

@giorgio-zamparelli:也许误解源于我使用了短语 _"flush all pending Promise handlers"_ (如果是这样,我很抱歉)。 如果您仔细阅读了问题描述,希望您会看到,我的意思是“未决的承诺处理程序”。

所以,重申一下,我们不是在谈论 _pending_ 承诺在这里(以任何方式),而是以最小的麻烦刷新承诺解决方案。 或者,换句话说,从承诺的解决点到所有与它相关的后续效果被调用的点(以便我们可以测试其结果),透明地和确定性地获得。

我最近为此发布了flush-microtasks 。 它借用其实现从反应,这是令人惊讶的比@jwbay更复杂的解决方案在这里@thymikee的解决方案在这里。 我不确定复杂性是否会产生任何有意义的差异,但我认为它考虑了该线程中其他解决方案未考虑的边缘情况。 我只使用了那个实现,因为react-testing-library使用了它(见这里),但没有公开它。

import { flushMicroTasks } from 'flush-microtasks'

await flushMicroTasks()

@aleclarson刷新微任务和刷新承诺之间有什么区别吗

@ramusus看起来flush-promises使用与@jwbay的解决方案相同的方法。

https://github.com/kentor/flush-promises/blob/46f58770b14fb74ce1ff27da00837c7e722b9d06/index.js

RTL 也复制了 React 的代码: https :

编辑:哈,已经提到:咧嘴笑:

当人们可以使用它时,我不确定我们是否需要将它构建到 Jest 中? 也许我们可以在文档中链接到它? 这个问题是关于同步刷新它们,我认为这超出了我们想要做的(特别是因为async-await是不可能的)

flushPromises解决方案仅适用于立即解决的承诺,但不适用于仍待处理的承诺。

嗯,说得好。 我不知道是否有可能以某种方式跟踪pending承诺。 可能可以用async_hooks做一些聪明的事情,不确定。 试图区分由用户代码创建的承诺和由 Jest 及其依赖项创建的承诺可能会很痛苦

我试图包装/模拟Promise对象以包含一个计数器,但这不起作用:

const _promise = window.Promise;
window.Promise = function(promiseFunction){
    // counter
    return new _promise(promiseFunction);
}

主要问题是async函数根本不使用全局Promise

好的,我找到了一种像这样的非常hacky的方式。

  1. 使用列表创建一个新模块。
  2. 将您的承诺添加到该列表中。
  3. 解决测试中的 Promise 并将它们从列表中删除。
  4. 就我而言,我从酶中运行wrapper.update() 。 如有必要,请在此处执行类似的操作。
  5. 重复步骤 3 和 4,直到列表为空。

我知道,根据测试调整代码不是一个好习惯,但我已经在服务器端渲染中使用了这个逻辑。 但最终只能等待。 ¯\_(ツ)_/¯

在 Jest 26 中有一个有趣的更新,其中假计时器现在基于 @sinon/fake-timers (如果启用了jest.useFakeTimers('modern') )。

我在测试中尝试了现代假定时器,不幸的是它导致await new Promise(resolve => setImmediate(resolve)); hack 无限期挂起。 幸运的是, @sinon/fake-timers包括几个*Async()方法,它们“也将打破事件循环,允许任何计划的承诺回调在运行计时器之前执行_before_。”。 不幸的是,我看不到通过 Jest API 获取clock对象的任何方法。

有人知道如何让 Jest 给我们那个clock对象吗?

和其他人一样,我使用await new Promise(setImmediate);动机是刷新可解析的 promise,以便我可以对它们对系统的影响进行单元测试。

通过看似荒谬的超时,“现代”假计时器似乎确实比其他计时器表现不佳。

这里有一些单元测试来描述这个:

describe('flushing of js-queues using different timers', () => {
  beforeAll(() => {
    // It would take the failing test 5 long seconds to time out.
    jest.setTimeout(100);
  });

  it.each([
    [
      'given real timers',
      () => {
        jest.useRealTimers();
      },
    ],
    ['given no timers', () => {}],
    [
      'given "legacy" fake timers',
      () => {
        jest.useFakeTimers('legacy');
      },
    ],
    [
      // This is the the failing scenario, not working like the other timers.
      'given "modern" fake timers',
      () => {
        jest.useFakeTimers('modern');
      },
    ],
  ])(
    '%s, when using setImmediate to flush, flushes a promise without timing out',
    async (_, initializeScenarioSpecificTimers) => {
      initializeScenarioSpecificTimers();

      let promiseIsFlushed = false;

      Promise.resolve().then(() => {
        promiseIsFlushed = true;
      });

      // Flush promises
      await new Promise(setImmediate);

      expect(promiseIsFlushed).toBe(true);
    },
  );
});

我觉得之前的测试不应该像现在这样失败。

对我来说,解决方法是使用“timers”包中的节点原生“setImmediate”而不是全局“setImmediate”来刷新承诺。 有了这个,以下通过:

import { setImmediate as flushMicroTasks } from 'timers';

it('given "modern" fake timers, when using native timers to flush, flushes a promise without timing out', async () => {
  jest.useFakeTimers('modern');

  let promiseIsFlushed = false;

  Promise.resolve().then(() => {
    promiseIsFlushed = true;
  });

  // Flush micro and macro -tasks
  await new Promise(flushMicroTasks);

  expect(promiseIsFlushed).toBe(true);
});

谢谢@aleclarson。

这是我们针对此问题的解决方案:

https://github.com/team-igniter-from-houston-inc/async-fn
https://medium.com/houston-io/how-to-unit-test-asynchronous-code-for-javascript-in-2020-41c124be2552

测试代码可以这样写:

// Note: asyncFn(), extends jest.fn() with a way to control resolving/rejecting of a promise
const load = asyncFn();

const afterLoad = jest.fn();
const result = 'mock result';

mount(<Component load={load} afterLoad={afterLoad} />);

// ... some interaction that requires the `load`

// Note: New way to controlling when promise resolves
await load.resolve(result);

expect(afterLoad).toHaveBeenCalledWith(result);

请注意,您无需了解有关刷新承诺或运行计时器的任何信息。

@jansav不错/+1。 Fwiw 我见过这种方法称为延迟模式。 我认为它确实可以进行更好的测试。

在我看来,假定时器的问题在于它破坏了定时器应该如何工作的自然运行循环。 我想知道为什么我们不能简单地让 Jest 计时器运行函数异步? 更改计时器以同步解决确实使测试代码看起来很整洁,但它会导致这种巨大的副作用。

我的用例:

public static resolvingPromise<T>(result: T, delay: number = 5): Promise<T> {
    return new Promise((resolve) => {
        setTimeout(
            () => {
                resolve(result);
            },
            delay
        );
    });
}

测试文件:

it("accepts delay as second parameter", async () => {
    const spy = jest.fn();
    MockMiddleware.resolvingPromise({ mock: true }, 50).then(spy);
    jest.advanceTimersByTime(49);
    expect(spy).not.toHaveBeenCalled();
    jest.advanceTimersByTime(1);
    await Promise.resolve(); // without this line, this test won't pass
    expect(spy).toHaveBeenCalled();
});
此页面是否有帮助?
0 / 5 - 0 等级