Yihui’s Blog

Vue test方法

在现代前端开发中,Vue.js 因其简洁和灵活性被广泛采用。而随着应用的复杂性增加,测试也变得越来越重要。Vue Test Utils(简称 VTU)是一个专为 Vue.js 设计的测试库,它提供了一系列功能强大的工具,使得开发者可以更方便地对 Vue 组件进行单元测试和集成测试。本文将详细探讨 VTU 的功能、使用方法以及其在 Vue.js 测试中的重要性。

VTU 的核心功能

VTU 提供了一套完整的 API,用于挂载和测试 Vue 组件。这些 API 主要分为以下几类:

1. 挂载组件

VTU 提供两个核心的挂载方法:mount 和 shallowMount。其中:

  • mount 会完整地挂载组件及其子组件。适用于组件本身和子组件之间存在较多交互,需要整体进行测试的场景。
  • shallowMount 则仅挂载组件本身,而对子组件进行浅层的处理(即,用占位符代替子组件)。这种方式可以隔离组件,专注于组件内部的逻辑和状态,从而加快测试速度。
import { mount, shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

const wrapper = mount(MyComponent);
const shallowWrapper = shallowMount(MyComponent);

2. 找到元素

VTU 提供了多种查找 DOM 元素的方法,例如 find、findAll、findComponent 和 findAllComponents 等。这些方法允许开发者通过 CSS 选择器、组件名称等来定位和操作组件的子元素或子组件。

const button = wrapper.find('button');
const paragraphs = wrapper.findAll('p');
const childComponent = wrapper.findComponent({ name: 'ChildComponent' });

不可见元素

使用 isVisible()

isVisible() 提供检查隐藏元素的能力。特别是 isVisible() 将检查是否:

  • 元素或其祖先具有display: none,visibility: hidden,opacity :0 样式
  • 元素或其祖先位于折叠<details>标记内
  • 元素或其祖先具有 hidden 属性

使用 v-show 的测试场景如下所示:

test('does not show the user dropdown', () => {
  const wrapper = mount(Nav)
  expect(wrapper.get('#user-dropdown').isVisible()).toBe(false)
})

3. 事件模拟与事件处理

在测试中,我们常常需要模拟用户交互。VTU 提供了如 trigger 等一系列方法来模拟事件,使得测试更贴近实际应用场景。

await button.trigger('click');

Vue 组件通过 prop 和通过调用 $emit 发出事件来相互交互。在本指南中,我们将介绍如何使用 emitted() 函数验证事件是否正确发出。

const Counter = {
  template: '<button @click="handleClick">Increment</button>',
  data() {
    return {
      count: 0
    }
  },
  methods: {
    handleClick() {
      this.count += 1
      this.$emit('increment', this.count)
    }
  }

emitted() 方法。它返回一个对象,其中包含组件发出的所有事件,以及数组中的参数。让我们看看它是如何工作的:

test('emits an event when clicked', () => {
  const wrapper = mount(Counter)

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

  expect(wrapper.emitted()).toHaveProperty('increment')
})

我们需要检查在调用 this.$emit('increment', this.count) 时是否发出正确的参数。

我们的下一步是断言事件包含 count 值。我们通过向 emitted() 传递一个参数来实现这一点。

test('emits an event with count when clicked', () => {
  const wrapper = mount(Counter)

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

  // `emitted()` accepts an argument. It returns an array with all the
  // occurrences of `this.$emit('increment')`.
  const incrementEvent = wrapper.emitted('increment')

  // We have "clicked" twice, so the array of `increment` should
  // have two values.
  expect(incrementEvent).toHaveLength(2)

  // Assert the result of the first click.
  // Notice that the value is an array.
  expect(incrementEvent[0]).toEqual([1])

  // Then, the result of the second one.
  expect(incrementEvent[1]).toEqual([2])
})

断言复杂事件:

4. 表单处理

Vue 中的表单可以像纯 HTML 表单一样简单,也可以像自定义 Vue 组件表单元素的复杂嵌套树一样简单。我们将逐步了解与表单元素交互、设置值和触发事件的方式。我们最常使用的方法是 setValue() 和 trigger()。

要更改 VTU 中输入的值,您可以使用 setValue() 方法。它接受一个参数,通常是一个 String 或 Boolean,并返回一个 Promise,它在 Vue 更新 DOM 后解析。

test('sets the value', async () => {
  const wrapper = mount(Component)
  const input = wrapper.find('input')

  await input.setValue('my@mail.com')

  expect(input.element.value).toBe('my@mail.com')
})

正如您所看到的,setValue 将输入元素上的value 属性设置为我们传递给它的值。在我们做出任何断言之前,我们使用 await 来确保 Vue 已完成更新并且更改已反映在 DOM 中。setValue是一种非常通用的方法。它可以与所有类型的表单元素一起使用。

在上面的表单中,我们将事件从 button 移动到 form 元素。这是一个值得遵循的好习惯,因为这允许您通过点击 enter 键来提交表单,这是一种更原生的方法。为了触发 submit 处理程序,我们再次使用 trigger 方法。

test('submits the form', async () => {
  const wrapper = mount(FormComponent)

  const email = 'name@mail.com'
  const description = 'Lorem ipsum dolor sit amet'
  const city = 'moscow'

  await wrapper.find('input[type=email]').setValue(email)
  await wrapper.find('textarea').setValue(description)
  await wrapper.find('select').setValue(city)
  await wrapper.find('input[type=checkbox]').setValue()
  await wrapper.find('input[type=radio][value=monthly]').setValue()

  await wrapper.find('form').trigger('submit.prevent')

  expect(wrapper.emitted('submit')[0][0]).toStrictEqual({
    email,
    description,
    city,
    subscribe: true,
    interval: 'monthly'
  })
})

测试复杂的输入组件:

如果您的输入组件不那么简单会发生什么?您可能正在使用 UI 库,例如 Vuetify。如果您依赖挖掘标记内部来查找正确的元素,那么如果外部库决定更改其内部结构,您的测试可能会中断。

In such cases you can set the value directly, using the component instance and setValue.在这种情况下,您可以使用组件实例和 setValue 直接设置值。

Assume we have a form that uses the Vuetify textarea:假设我们有一个使用 Vuetify 文本区域的表单:

vue视图

<template>
  <form @submit.prevent="handleSubmit">
    <v-textarea v-model="description" ref="description" />
    <button type="submit">Send</button>
  </form>
</template>

<script>
export default {
  name: 'CustomTextarea',
  data() {
    return {
      description: ''
    }
  },
  methods: {
    handleSubmit() {
      this.$emit('submitted', this.description)
    }
  }
}
</script>

We can use findComponent to find the component instance, and then set its value.我们可以使用findComponent来查找组件实例,然后设置它的值。

jsjs

test('emits textarea value on submit', async () => {
  const wrapper = mount(CustomTextarea)
  const description = 'Some very long text...'

  await wrapper.findComponent({ ref: 'description' }).setValue(description)

  wrapper.find('form').trigger('submit')

  expect(wrapper.emitted('submitted')[0][0]).toEqual(description)
})

5. 快照测试

快照测试是一种将组件的渲染输出进行快照记录的方法。VTU 与 Jest 等工具配合使用时,可以方便地进行快照测试,确保组件的 UI 没有意外变化。

expect(wrapper.html()).toMatchSnapshot();

6. 测试服务器端渲染

renderToString 是一个将 Vue 组件渲染为字符串的函数。它是一个异步函数,返回一个 Promise,并接受与 mount 或 shallowMount 相同的参数。

让我们考虑一个使用 onServerPrefetch 钩子的简单组件:

function fakeFetch(text: string) {
  return Promise.resolve(text)
}

const Component = defineComponent({
  template: '<div>{{ text }}</div>',
  setup() {
    const text = ref<string | null>(null)

    onServerPrefetch(async () => {
      text.value = await fakeFetch('onServerPrefetch')
    })

    return { text }
  }
})

可以使用 renderToString 为此组件编写测试:

import { renderToString } from '@vue/test-utils'

it('renders the value returned by onServerPrefetch', async () => {
  const contents = await renderToString(Component)
  expect(contents).toBe('<div>onServerPrefetch</div>')
})

三、编写易于测试的组件

四、VTU 的优势

VTU 的诞生为 Vue.js 组件的测试带来了诸多优势:

  1. 简化测试流程:VTU 的 API 设计简洁且易于使用,极大地简化了测试流程,使得开发者可以专注于编写高质量的测试用例。
  2. 高效的测试执行:shallowMount 方法提供了高效的测试方式,避免了不必要的子组件渲染,从而加快了测试执行速度。
  3. 灵活的测试场景:VTU 提供了丰富的 API,可以满足各种测试需求,无论是简单的单元测试,还是复杂的集成测试。
  4. 社区支持:作为 Vue 官方推荐的测试工具,VTU 拥有强大的社区支持和不断更新的文档,使得开发者可以及时获取帮助和更新。

五、实践中的 VTU

在实际项目中,VTU 的应用主要体现在以下几个方面:

  1. 单元测试:通过单元测试,可以验证组件在隔离环境下的行为,例如方法调用、数据处理和事件响应。
  2. 集成测试:通过集成测试,可以验证多个组件之间的交互和数据流,例如父子组件的通信、Vuex 状态管理等。
  3. 快照测试:通过快照测试,可以确保组件的 UI 输出符合预期,防止出现意外变化。

以下是一个简单的单元测试示例:

import { shallowMount } from '@vue/test-utils';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  it('renders a message', () => {
    const wrapper = shallowMount(MyComponent, {
      propsData: { message: 'Hello World' }
    });

    expect(wrapper.text()).toContain('Hello World');
  });

  it('increments count when button is clicked', async () => {
    const wrapper = shallowMount(MyComponent);
    const button = wrapper.find('button');
    await button.trigger('click');

    expect(wrapper.vm.count).toBe(1);
  });
});

总结

VTU 作为 Vue.js 的测试工具,为开发者提供了一套强大且易用的 API,使得组件测试变得更加高效和便捷。通过 VTU,开发者可以在提高代码质量、减少 bug 和提升用户体验等方面获得显著的成果。在未来的开发过程中,掌握和使用 VTU 将成为每一个 Vue.js 开发者的重要技能。希望本文能够帮助读者深入了解 VTU 的功能和使用方法,为 Vue.js 项目的测试提供有力支持。

Maintained by · YihuiEdit on GitHub