您要请求功能还是报告错误?
_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);
我试过runAllTicks
和runAllTimers
没有效果。
_或者,如果我只是错过了一些已经存在的功能或模式,我希望这里有人能指出我正确的方向:)_
虽然异步测试承诺是好的,但记住你可以将测试函数作为 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的方式。
wrapper.update()
。 如有必要,请在此处执行类似的操作。我知道,根据测试调整代码不是一个好习惯,但我已经在服务器端渲染中使用了这个逻辑。 但最终只能等待。 ¯\_(ツ)_/¯
在 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();
});
最有用的评论
辅助函数可以将其转换为承诺本身,因此您无需处理 done 回调。 它足够小,放在用户区是无害的,但如果把它放在开玩笑的对象上,我不会抱怨。 像这样的东西在我的项目中得到了很多使用。
使用 async await 它几乎很漂亮: