Skip to content
该翻译已同步到了 的版本,其对应的 commit hash 是 7c55128
同时该文档仍处于校对中,如有任何疑问或想参与校对工作,请移步这里了解更多。

异步行为

你可能注意到在指南的某些部分,在调用 wrapper 的一些方法时使用了 await,例如 triggersetValue。这是什么意思呢?

你可能知道 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 不会在下一个事件循环的 tick 之前更新 DOM。因此,断言 (expect()...) 会在 Vue 更新 DOM 之前被调用。

TIP

如果你想了解更多关于这个核心 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')
})

现在的测试将会通过,因为我们确保在断言运行之前,下一个 "tick" 已经执行并且 DOM 已经更新。

由于 await nextTick() 是常见的,Vue Test Utils 提供了一个快捷方式。导致 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 将不起作用——你的断言可能在 Promise 被解决之前就运行了。对于这种情况,Vue Test Utils 提供了 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 () => {
  // 某个在 `created` 中使用 `axios` 进行 HTTP 调用的组件
  const wrapper = mount(AxiosComponent)

  await flushPromises() // axios 的 Promise 被立即解决

  // 在上面的代码执行后,axios 请求已使用模拟数据解决。
})

TIP

如果你想了解更多关于在组件上测试请求的信息,请确保查看发起 HTTP 请求 指南。

测试异步 setup

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

例如,这个 Async 组件:

js
const Async = defineComponent({
  async setup() {
    // 等待某些内容
  }
})

必须这样测试:

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) 返回的 wrapper 包含它自己的(空的)vm

结论

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

Released under the MIT License.