Skip to content

异步行为

¥Asynchronous Behavior

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

¥You may have noticed some other parts of the guide using await when calling some methods on wrapper, such as trigger and setValue. What's that all about?

你可能知道 Vue 是被动更新的:当你更改值时,DOM 会自动更新以反映最新值。Vue 异步执行这些更新。相比之下,像 Jest 这样的测试运行程序是同步运行的。这可能会导致测试中出现一些令人惊讶的结果。

¥You might know Vue updates reactively: when you change a value, the DOM is automatically updated to reflect the latest value. Vue does these updates asynchronously. In contrast, a test runner like Jest runs synchronously. This can cause some surprising results in tests.

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

¥Let's look at some strategies to ensure Vue is updating the DOM as expected when we run our tests.

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

¥A Simple Example - Updating with trigger

让我们通过一次更改重新使用 事件处理 中的 <Counter> 组件;我们现在在 template 中渲染 count

¥Let's re-use the <Counter> component from event handling with one change; we now render the count in the template.

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

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

¥Let's write a test to verify the count is increasing:

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 之前被调用。

¥Surprisingly, this fails! The reason is although count is increased, Vue will not update the DOM until the next event loop tick. For this reason, the assertion (expect()...) will be called before Vue updates the DOM.

提示

如果你想了解有关此核心 JavaScript 行为的更多信息,请阅读 事件循环及其宏任务和微任务

¥If you want to learn more about this core JavaScript behavior, read about the Event Loop and its macrotasks and microtasks.

除了实现细节之外,我们如何解决这个问题?Vue 实际上为我们提供了一种等待 DOM 更新的方法:nextTick

¥Implementation details aside, how can we fix this? Vue actually provides a way for us to wait until the DOM is updated: 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 已经更新。

¥Now the test will pass because we ensure the next "tick" has been executed and the DOM has been updated before the assertion runs.

由于 await nextTick() 很常见,Vue Test Utils 提供了一个快捷方式。导致 DOM 更新的方法,例如 triggersetValue 返回 nextTick,因此你可以直接 await 这些方法:

¥Since await nextTick() is common, Vue Test Utils provides a shortcut. Methods that cause the DOM to update, such as trigger and setValue return nextTick, so you can just await those directly:

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

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

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

解决其他异步行为

¥Resolving Other Asynchronous Behavior

nextTick 对于确保在继续测试之前反应数据的某些更改反映在 DOM 中非常有用。但是,有时你可能希望确保其他非 Vue 相关的异步行为也完成。

¥nextTick is useful to ensure some change in reactive data is reflected in the DOM before continuing the test. However, sometimes you may want to ensure other, non Vue-related asynchronous behavior is completed, too.

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

¥A common example is a function that returns a Promise. Perhaps you mocked your axios HTTP client using jest.mock:

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

在这种情况下,Vue 不知道未解决的 Promise,因此调用 nextTick 将不起作用 - 你的断言可能会在解决之前运行。对于这样的场景,Vue Test Utils 公开了 flushPromises,这会导致所有未完成的 promise 立即解决。

¥In this case, Vue has no knowledge of the unresolved Promise, so calling nextTick will not work - your assertion may run before it is resolved. For scenarios like this, Vue Test Utils exposes flushPromises, which causes all outstanding promises to resolve immediately.

让我们看一个例子:

¥Let's see an example:

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 请求 指南。

¥If you want to learn more about testing requests on Components, make sure you check Making HTTP Requests guide.

测试异步 setup

¥Testing asynchronous setup

如果你要测试的组件使用异步 setup,则必须将该组件挂载在 Suspense 组件内(就像在应用中使用它时所做的那样)。

¥If the component you want to test uses an asynchronous setup, then you must mount the component inside a Suspense component (as you do when you use it in your application).

例如,这个 Async 组件:

¥For example, this Async component:

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

必须进行如下测试:

¥must be tested as follow:

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

¥Note: To access your Async components' underlying vm instance, use the return value of wrapper.findComponent(Async). Since a new component is defined and mounted in this scenario, the wrapper returned by mount(TestComponent) contains its' own (empty) vm.

结论

¥Conclusion

  • Vue 异步更新 DOM;测试运行程序改为同步执行代码。

    ¥Vue updates the DOM asynchronously; tests runner executes code synchronously instead.

  • 在测试继续之前,使用 await nextTick() 确保 DOM 已更新。

    ¥Use await nextTick() to ensure the DOM has updated before the test continues.

  • 可能更新 DOM 的函数(如 triggersetValue)返回 nextTick,因此你需要对它们进行 await

    ¥Functions that might update the DOM (like trigger and setValue) return nextTick, so you need to await them.

  • 使用 Vue Test Utils 中的 flushPromises 来解决非 Vue 依赖(例如 API 请求)中任何未解决的 promise。

    ¥Use flushPromises from Vue Test Utils to resolve any unresolved promises from non-Vue dependencies (such as API requests).

  • 使用 Suspense 在异步测试函数中使用异步 setup 来测试组件。

    ¥Use Suspense to test components with an asynchronous setup in an asynchronous test function.

Vue Test Utils 中文网 - 粤ICP备13048890号