jest.fn() 声称没有被调用,但有

mcv*_*mcv 8 vue.js jestjs vuex vuejs2

我正在测试一个 Vue 组件,当路由中存在某个参数时,该组件会在我的 Vuex 存储中调用某个操作。我在嘲笑这个动作jest.fn()

这是组件中的相关代码:

await this.$store.dispatch('someOtherAction');
if (this.$route.params && this.$route.params.id) {
    this.$store.dispatch('selection/selectElement', parseInt(this.$route.params.id, 10));
}
Run Code Online (Sandbox Code Playgroud)

这是模拟函数:

someOtherAction = jest.fn();
selectElement = jest.fn(() => console.log("selectElement has been called"));
Run Code Online (Sandbox Code Playgroud)

我的测试:

it('selects element if passed in route', async () => {
  const $route = {params: {id: '256'}};
  const wrapper = shallowMount(AbcModel, {
    mocks: {$route},
    store, localVue
  });
  expect(someOtherAction).toHaveBeenCalled();
  expect(selectElement).toHaveBeenCalled();
});
Run Code Online (Sandbox Code Playgroud)

在输出中,我可以看到“selectElement 已被调用”。显然它已经被调用了。然而,expect(selectElement).toHaveBeenCalled()失败了。

这怎么可能?它适用于我嘲笑的另一个函数。替换我模拟函数的顺序并不重要。消除调用另一个函数的期望也无关紧要,因此它看起来不像冲突。

Bri*_*ams 12

这怎么可能?

expect之前的runs和failureselectElement都有机会跑了。


细节

消息队列

JavaScript 使用消息队列。当前消息在下一个开始之前运行到完成

PromiseJobs 队列

ES6 引入了PromiseJobs 队列,它处理“对 Promise 的解决做出响应”的作业。PromiseJobs 队列中的任何作业都在当前消息完成之后和下一条消息开始之前运行。

异步/等待

async并且await只是promises 和 generators 的语法糖。调用awaitaPromise本质上将函数的其余部分包装在回调中,以便在解析时在 PromiseJobs 中进行调度Promise

发生什么了

您的测试开始作为当前正在运行的消息运行。调用shallowMount加载您的组件,该组件运行直到await this.$store.dispatch('someOtherAction');调用someOtherFunction,然后基本上将函数的其余部分作为Promise回调排队,以便在Promise解决时在 PromiseJobs 队列中进行调度。

执行然后返回到运行这两个expect语句的测试。第一个通过,因为someOtherFunction已被调用,但第二个失败,因为selectElement尚未运行。

然后当前正在运行的消息完成,然后运行 ​​PromiseJobs 队列中的挂起作业。调用的回调selectElement位于队列中,因此它运行并调用selectElement记录到控制台的回调。


解决方案

确保Promise该调用回调selectElement已运行之前运行expect

只要有可能,最好返回Promise以便测试可以await直接进行。

如果这是不可能的,那么解决方法是在测试期间调用await一个已解决的方法Promise,它基本上将其余的测试排在 PromiseJobs 队列的后面,并允许Promise首先运行任何挂起的回调:

it('selects element if passed in route', async () => {
  const $route = {params: {id: '256'}};
  const wrapper = shallowMount(AbcModel, {
    mocks: {$route},
    store, localVue
  });
  expect(someOtherFunction).toHaveBeenCalled();
  // Ideally await the Promise directly...
  // but if that isn't possible then calling await Promise.resolve()
  // queues the rest of the test at the back of PromiseJobs
  // allowing any pending callbacks to run first
  await Promise.resolve();
  expect(selectElement).toHaveBeenCalled();  // SUCCESS
});
Run Code Online (Sandbox Code Playgroud)