跳至内容

异步行为

您可能已经注意到指南的其他部分在调用 wrapper 上的一些方法(例如 triggersetValue)时使用了 await。这是怎么回事呢?

您可能知道 Vue 是响应式更新的:当您更改一个值时,DOM 会自动更新以反映最新的值。 Vue 异步执行这些更新。相反,像 Jest 这样的测试运行器是同步运行的。这可能会导致测试中出现一些令人惊讶的结果。

让我们看看一些策略来确保 Vue 在运行测试时按预期更新 DOM。

一个简单的例子 - 使用 trigger 更新

让我们重新使用来自 事件处理<Counter> 组件,并进行一个更改;我们现在在 template 中渲染 count

js
const Counter = {
  template: `
    <p>Count: {{ count }}</p>
    <button @click="handleClick">Increment</button>
  `,
  data() {
    return {
      count: 0
    }
  },
  methods: {
    handleClick() {
      this.count += 1
    }
  }
}

让我们编写一个测试来验证 count 是否在增加

js
test('increments by 1', () => {
  const wrapper = mount(Counter)

  wrapper.find('button').trigger('click')

  expect(wrapper.html()).toContain('Count: 1')
})

令人惊讶的是,这失败了!原因是虽然 count 增加了,但 Vue 不会在下一个事件循环滴答声之前更新 DOM。因此,断言 (expect()...) 将在 Vue 更新 DOM 之前被调用。

提示

如果您想了解更多关于这种核心 JavaScript 行为的信息,请阅读有关 事件循环及其宏任务和微任务 的内容。

撇开实现细节不谈,我们如何解决这个问题呢?Vue 实际上提供了一种方法让我们等待 DOM 更新:nextTick

js
import { nextTick } from 'vue'

test('increments by 1', async () => {
  const wrapper = mount(Counter)

  wrapper.find('button').trigger('click')
  await nextTick()

  expect(wrapper.html()).toContain('Count: 1')
})

现在测试将通过,因为我们确保在断言运行之前执行了下一个“滴答声”,并且 DOM 已更新。

由于 await nextTick() 很常见,Vue 测试工具提供了一个快捷方式。导致 DOM 更新的方法(例如 triggersetValue)返回 nextTick,因此您只需直接 await 它们即可

js
test('increments by 1', async () => {
  const wrapper = mount(Counter)

  await wrapper.find('button').trigger('click')

  expect(wrapper.html()).toContain('Count: 1')
})

解决其他异步行为

nextTick 用于确保在继续测试之前,响应式数据中的某些更改反映在 DOM 中。但是,有时您可能还想确保其他与 Vue 无关的异步行为也已完成。

一个常见的例子是一个返回 Promise 的函数。也许您使用 jest.mock 模拟了您的 axios HTTP 客户端

js
jest.spyOn(axios, 'get').mockResolvedValue({ data: 'some mocked data!' })

在这种情况下,Vue 不知道未解析的 Promise,因此调用 nextTick 将不起作用 - 您的断言可能会在它解析之前运行。对于这种情况,Vue 测试工具公开了 flushPromises,它会导致所有未决的 Promise 立即解析。

让我们看一个例子

js
import { flushPromises } from '@vue/test-utils'
import axios from 'axios'

jest.spyOn(axios, 'get').mockResolvedValue({ data: 'some mocked data!' })

test('uses a mocked axios HTTP client and flushPromises', async () => {
  // some component that makes a HTTP called in `created` using `axios`
  const wrapper = mount(AxiosComponent)

  await flushPromises() // axios promise is resolved immediately

  // after the line above, axios request has resolved with the mocked data.
})

提示

如果您想了解更多关于测试组件上的请求的信息,请务必查看 发出 HTTP 请求 指南。

测试异步 setup

如果您要测试的组件使用异步 setup,那么您必须在 Suspense 组件内挂载该组件(就像您在应用程序中使用它时一样)。

例如,这个 Async 组件

js
const Async = defineComponent({
  async setup() {
    // await something
  }
})

必须按如下方式测试

js
test('Async component', async () => {
  const TestComponent = defineComponent({
    components: { Async },
    template: '<Suspense><Async/></Suspense>'
  })

  const wrapper = mount(TestComponent)
  await flushPromises();
  // ...
})

注意:要访问 Async 组件的底层 vm 实例,请使用 wrapper.findComponent(Async) 的返回值。由于在这种情况下定义并挂载了一个新组件,因此由 mount(TestComponent) 返回的包装器包含它自己的(空的)vm

结论

  • Vue 异步更新 DOM;测试运行器则同步执行代码。
  • 使用 await nextTick() 来确保在测试继续之前 DOM 已更新。
  • 可能更新 DOM 的函数(如 triggersetValue)返回 nextTick,因此您需要 await 它们。
  • 使用 Vue 测试工具中的 flushPromises 来解析来自非 Vue 依赖项(如 API 请求)的任何未决的 Promise。
  • 使用 Suspense 来测试具有异步 setup 的组件,并在异步测试函数中测试。