速成课程
让我们直接开始吧!让我们通过构建一个简单的待办事项应用程序并边写边测试来学习 Vue 测试工具 (VTU)。本指南将涵盖如何
- 挂载组件
- 查找元素
- 填写表单
- 触发事件
入门
我们将从一个简单的 TodoApp
组件开始,其中包含一个待办事项
<template>
<div></div>
</template>
<script>
export default {
name: 'TodoApp',
data() {
return {
todos: [
{
id: 1,
text: 'Learn Vue.js 3',
completed: false
}
]
}
}
}
</script>
第一个测试 - 渲染一个待办事项
我们将编写的第一个测试将验证是否渲染了一个待办事项。让我们先看看测试,然后讨论每个部分
import { mount } from '@vue/test-utils'
import TodoApp from './TodoApp.vue'
test('renders a todo', () => {
const wrapper = mount(TodoApp)
const todo = wrapper.get('[data-test="todo"]')
expect(todo.text()).toBe('Learn Vue.js 3')
})
我们首先导入 mount
- 这是在 VTU 中渲染组件的主要方式。您使用带有测试简短描述的 test
函数来声明测试。test
和 expect
函数在大多数测试运行器中全局可用(此示例使用 Jest)。如果 test
和 expect
看起来令人困惑,Jest 文档有一个 更简单的示例 说明如何使用它们以及它们的工作原理。
接下来,我们调用 mount
并将组件作为第一个参数传递 - 这是您编写的几乎所有测试都将执行的操作。按照惯例,我们将结果分配给一个名为 wrapper
的变量,因为 mount
为应用程序提供了一个简单的“包装器”,并提供了一些方便的测试方法。
最后,我们使用另一个在许多测试运行器(包括 Jest)中常见的全局函数 - expect
。我们的想法是,我们断言或期望实际输出与我们认为应该的输出相匹配。在本例中,我们正在查找具有选择器 data-test="todo"
的元素 - 在 DOM 中,这将类似于 <div data-test="todo">...</div>
。然后我们调用 text
方法来获取内容,我们期望它为 'Learn Vue.js 3'
。
使用
data-test
选择器不是必需的,但它可以使您的测试更不容易出错。类和 ID 往往会随着应用程序的增长而发生变化或移动 - 通过使用data-test
,其他开发人员可以清楚地知道哪些元素用于测试,并且不应更改。
使测试通过
如果我们现在运行此测试,它将失败并显示以下错误消息:无法获取 [data-test="todo"]
。这是因为我们没有渲染任何待办事项,因此 get()
调用无法返回包装器(请记住,VTU 将所有组件和 DOM 元素包装在具有某些方便方法的“包装器”中)。让我们更新 TodoApp.vue
中的 <template>
以渲染 todos
数组
<template>
<div>
<div v-for="todo in todos" :key="todo.id" data-test="todo">
{{ todo.text }}
</div>
</div>
</template>
有了这个更改,测试就通过了。恭喜!您编写了第一个组件测试。
添加一个新的待办事项
我们将添加的下一个功能是让用户能够创建一个新的待办事项。为此,我们需要一个表单,其中包含一个输入框供用户输入一些文本。当用户提交表单时,我们期望渲染新的待办事项。让我们看看测试
import { mount } from '@vue/test-utils'
import TodoApp from './TodoApp.vue'
test('creates a todo', () => {
const wrapper = mount(TodoApp)
expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(1)
wrapper.get('[data-test="new-todo"]').setValue('New todo')
wrapper.get('[data-test="form"]').trigger('submit')
expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
})
像往常一样,我们首先使用 mount
来渲染元素。我们还断言只渲染了 1 个待办事项 - 这清楚地表明我们正在添加一个额外的待办事项,正如测试的最后一行所暗示的那样。
要更新 <input>
,我们使用 setValue
- 这使我们能够设置输入的值。
更新 <input>
后,我们使用 trigger
方法来模拟用户提交表单。最后,我们断言待办事项的数量已从 1 增加到 2。
如果我们运行此测试,它显然会失败。让我们更新 TodoApp.vue
以包含 <form>
和 <input>
元素,并使测试通过
<template>
<div>
<div v-for="todo in todos" :key="todo.id" data-test="todo">
{{ todo.text }}
</div>
<form data-test="form" @submit.prevent="createTodo">
<input data-test="new-todo" v-model="newTodo" />
</form>
</div>
</template>
<script>
export default {
name: 'TodoApp',
data() {
return {
newTodo: '',
todos: [
{
id: 1,
text: 'Learn Vue.js 3',
completed: false
}
]
}
},
methods: {
createTodo() {
this.todos.push({
id: 2,
text: this.newTodo,
completed: false
})
}
}
}
</script>
我们使用 v-model
来绑定到 <input>
,并使用 @submit
来监听表单提交。当表单提交时,将调用 createTodo
并将一个新的待办事项插入 todos
数组中。
虽然这看起来不错,但运行测试显示了一个错误
expect(received).toHaveLength(expected)
Expected length: 2
Received length: 1
Received array: [{"element": <div data-test="todo">Learn Vue.js 3</div>}]
待办事项的数量没有增加。问题是 Jest 以同步方式执行测试,并在最后一个函数被调用后立即结束测试。然而,Vue 以异步方式更新 DOM。我们需要将测试标记为 async
,并在可能导致 DOM 更改的任何方法上调用 await
。trigger
和 setValue
就是其中两种方法 - 我们只需在前面加上 await
,测试应该按预期工作
import { mount } from '@vue/test-utils'
import TodoApp from './TodoApp.vue'
test('creates a todo', async () => {
const wrapper = mount(TodoApp)
await wrapper.get('[data-test="new-todo"]').setValue('New todo')
await wrapper.get('[data-test="form"]').trigger('submit')
expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
})
现在测试终于通过了!
完成一个待办事项
现在我们能够创建待办事项了,让我们让用户能够使用复选框将待办事项标记为已完成/未完成。与之前一样,让我们从失败的测试开始
import { mount } from '@vue/test-utils'
import TodoApp from './TodoApp.vue'
test('completes a todo', async () => {
const wrapper = mount(TodoApp)
await wrapper.get('[data-test="todo-checkbox"]').setValue(true)
expect(wrapper.get('[data-test="todo"]').classes()).toContain('completed')
})
此测试与前两个测试类似;我们找到一个元素并以相同的方式与它交互(我们再次使用 setValue
,因为我们正在与 <input>
交互)。
最后,我们进行断言。我们将对已完成的待办事项应用 completed
类 - 然后我们可以使用它来添加一些样式,以直观地指示待办事项的状态。
我们可以通过更新 <template>
以包含 <input type="checkbox">
和待办事项元素上的类绑定来使此测试通过
<template>
<div>
<div
v-for="todo in todos"
:key="todo.id"
data-test="todo"
:class="[todo.completed ? 'completed' : '']"
>
{{ todo.text }}
<input
type="checkbox"
v-model="todo.completed"
data-test="todo-checkbox"
/>
</div>
<form data-test="form" @submit.prevent="createTodo">
<input data-test="new-todo" v-model="newTodo" />
</form>
</div>
</template>
恭喜!您编写了第一个组件测试。
安排、操作、断言
您可能已经注意到每个测试中的代码之间有一些新行。让我们详细看看第二个测试
import { mount } from '@vue/test-utils'
import TodoApp from './TodoApp.vue'
test('creates a todo', async () => {
const wrapper = mount(TodoApp)
await wrapper.get('[data-test="new-todo"]').setValue('New todo')
await wrapper.get('[data-test="form"]').trigger('submit')
expect(wrapper.findAll('[data-test="todo"]')).toHaveLength(2)
})
测试被分成三个不同的阶段,用新行隔开。这三个阶段代表测试的三个阶段:安排、操作和断言。
在安排阶段,我们正在为测试设置场景。更复杂的示例可能需要创建 Vuex 存储或填充数据库。
在操作阶段,我们执行场景,模拟用户如何与组件或应用程序交互。
在断言阶段,我们对组件的当前状态应该如何进行断言。
几乎所有测试都将遵循这三个阶段。您不需要像本指南那样用新行将它们隔开,但最好在编写测试时牢记这三个阶段。
结论
- 使用
mount()
来渲染组件。 - 使用
get()
和findAll()
来查询 DOM。 trigger()
和setValue()
是用于模拟用户输入的帮助程序。- 更新 DOM 是一个异步操作,因此请确保使用
async
和await
。 - 测试通常包含 3 个阶段;安排、操作和断言。