Appearance
速成课
¥A Crash Course
让我们直接开始吧!让我们通过构建一个简单的 Todo 应用并编写测试来学习 Vue Test Utils (VTU)。本指南将介绍如何:
¥Let's jump right into it! Let's learn Vue Test Utils (VTU) by building a simple Todo app and writing tests as we go. This guide will cover how to:
挂载组件
¥Mount components
查找元素
¥Find elements
填写表单
¥Fill out forms
触发事件
¥Trigger events
入门
¥Getting Started
我们将从一个简单的 TodoApp
组件开始,其中包含一个待办事项:
¥We will start off with a simple TodoApp
component with a single todo:
vue
<template>
<div></div>
</template>
<script>
export default {
name: 'TodoApp',
data() {
return {
todos: [
{
id: 1,
text: 'Learn Vue.js 3',
completed: false
}
]
}
}
}
</script>
第一次测试 - 渲染待办事项
¥The first test - a todo is rendered
我们将编写的第一个测试验证待办事项是否已渲染。我们先看一下测试,然后讨论各个部分:
¥The first test we will write verifies a todo is rendered. Let's see the test first, then discuss each part:
js
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 文档有 更简单的例子 介绍如何使用它们以及它们如何工作。
¥We start off by importing mount
- this is the main way to render a component in VTU. You declare a test by using the test
function with a short description of the test. The test
and expect
functions are globally available in most test runners (this example uses Jest). If test
and expect
look confusing, the Jest documentation has a more simple example of how to use them and how they work.
接下来,我们调用 mount
并将组件作为第一个参数传递 - 这几乎是你编写的每个测试都会做的事情。按照惯例,我们将结果分配给一个名为 wrapper
的变量,因为 mount
在应用周围提供了一个简单的 "wrapper" 以及一些方便的测试方法。
¥Next, we call mount
and pass the component as the first argument - this is something almost every test you write will do. By convention, we assign the result to a variable called wrapper
, since mount
provides a simple "wrapper" around the app with some convenient methods for testing.
最后,我们使用许多测试运行程序通用的另一个全局函数 - 包括 Jest - expect
。这个想法是我们断言或期望实际输出与我们认为应该的相匹配。在本例中,我们正在查找带有选择器 data-test="todo"
的元素 - 在 DOM 中,这看起来像 <div data-test="todo">...</div>
。然后我们调用 text
方法来获取内容,我们期望的是 'Learn Vue.js 3'
。
¥Finally, we use another global function common to many tests runner - Jest included - expect
. The idea is we are asserting, or expecting, the actual output to match what we think it should be. In this case, we are finding an element with the selector data-test="todo"
- in the DOM, this will look like <div data-test="todo">...</div>
. We then call the text
method to get the content, which we expect to be 'Learn Vue.js 3'
.
使用
data-test
选择器不是必需的,但它可以使你的测试不那么脆弱。随着应用的增长,类和 ID 往往会发生变化或移动 - 通过使用data-test
,其他开发者可以清楚哪些元素在测试中使用,并且不应更改。¥Using
data-test
selectors is not required, but it can make your tests less brittle. classes and ids tend to change or move around as an application grows - by usingdata-test
, it's clear to other developers which elements are used in tests, and should not be changed.
使测试通过
¥Making the test pass
如果我们现在运行此测试,它将失败并显示以下错误消息:Unable to get [data-test="todo"]
。这是因为我们没有渲染任何待办事项,因此 get()
调用无法返回封装器(请记住,VTU 使用一些方便的方法将所有组件和 DOM 元素封装在 "wrapper" 中)。让我们更新 TodoApp.vue
中的 <template>
以渲染 todos
数组:
¥If we run this test now, it fails with the following error message: Unable to get [data-test="todo"]
. That's because we aren't rendering any todo item, so the get()
call is failing to return a wrapper (remember, VTU wraps all components, and DOM elements, in a "wrapper" with some convenient methods). Let's update <template>
in TodoApp.vue
to render the todos
array:
vue
<template>
<div>
<div v-for="todo in todos" :key="todo.id" data-test="todo">
{{ todo.text }}
</div>
</div>
</template>
通过此更改,测试就通过了。恭喜!你编写了第一个组件测试。
¥With this change, the test is passing. Congratulations! You wrote your first component test.
添加新的待办事项
¥Adding a new todo
我们将添加的下一个功能是让用户能够创建新的待办事项。为此,我们需要一个带有输入的表单,供用户输入一些文本。当用户提交表单时,我们期望渲染新的待办事项。我们来看一下测试:
¥The next feature we will be adding is for the user to be able to create a new todo. To do so, we need a form with an input for the user to type some text. When the user submits the form, we expect the new todo to be rendered. Let's take a look at the test:
js
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 个待办事项 - 这清楚地表明我们正在添加一个额外的待办事项,正如测试的最后一行所示。
¥As usual, we start of by using mount
to render the element. We are also asserting that only 1 todo is rendered - this makes it clear that we are adding an additional todo, as the final line of the test suggests.
为了更新 <input>
,我们使用 setValue
- 这允许我们设置输入的值。
¥To update the <input>
, we use setValue
- this allows us to set the input's value.
更新 <input>
后,我们使用 trigger
方法来模拟用户提交表单。最后,我们断言待办事项的数量已从 1 增加到 2。
¥After updating the <input>
, we use the trigger
method to simulate the user submitting the form. Finally, we assert the number of todo items has increased from 1 to 2.
如果我们运行这个测试,它显然会失败。让我们更新 TodoApp.vue
以具有 <form>
和 <input>
元素并使测试通过:
¥If we run this test, it will obviously fail. Let's update TodoApp.vue
to have the <form>
and <input>
elements and make the test pass:
vue
<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
数组中。
¥We are using v-model
to bind to the <input>
and @submit
to listen for the form submission. When the form is submitted, createTodo
is called and inserts a new todo into the todos
array.
虽然这看起来不错,但运行测试会显示错误:
¥While this looks good, running the test shows an error:
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
,测试应该按预期工作:
¥The number of todos has not increased. The problem is that Jest executes tests in a synchronous manner, ending the test as soon as the final function is called. Vue, however, updates the DOM asynchronously. We need to mark the test async
, and call await
on any methods that might cause the DOM to change. trigger
is one such methods, and so is setValue
- we can simply prepend await
and the test should work as expected:
js
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)
})
现在测试终于通过了!
¥Now the test is finally passing!
完成待办事项
¥Completing a todo
现在我们可以创建待办事项,让用户能够使用复选框将待办事项标记为已完成/未完成。和之前一样,让我们从失败的测试开始:
¥Now that we can create todos, let's give the user the ability to mark a todo item as completed/uncompleted with a checkbox. As previously, let's start with the failing test:
js
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>
交互)。
¥This test is similar to the previous two; we find an element and interact with it in same way (we use setValue
again, since we are interacting with a <input>
).
最后,我们做出一个断言。我们将应用 completed
类来完成待办事项 - 然后我们可以使用它添加一些样式来直观地指示待办事项的状态。
¥Lastly, we make an assertion. We will be applying a completed
class to completed todos - we can then use this to add some styling to visually indicate the status of a todo.
我们可以通过更新 <template>
以包含 <input type="checkbox">
和 todo 元素上的类绑定来通过此测试:
¥We can get this test to pass by updating the <template>
to include the <input type="checkbox">
and a class binding on the todo element:
vue
<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>
恭喜!你编写了第一个组件测试。
¥Congratulations! You wrote your first component tests.
安排、行动、断言
¥Arrange, Act, Assert
你可能已经注意到每个测试中的代码之间有一些新行。让我们再详细看看第二个测试:
¥You may have noticed some new lines between the code in each of the tests. Let's look at the second test again, in detail:
js
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)
})
测试分为三个不同的阶段,由新线分隔。这三个阶段代表测试的三个阶段:安排、行动和断言。
¥The test is split into three distinct stages, separated by new lines. The three stages represent the three phases of a test: arrange, act and assert.
在安排阶段,我们正在设置测试场景。更复杂的示例可能需要创建 Vuex 存储或填充数据库。
¥In the arrange phase, we are setting up the scenario for the test. A more complex example may require creating a Vuex store, or populating a database.
在行动阶段,我们表演场景,模拟用户如何与组件或应用交互。
¥In the act phase, we act out the scenario, simulating how a user would interact with the component or application.
在断言阶段,我们对组件当前状态的预期做出断言。
¥In the assert phase, we make assertions about how we expect the current state of the component to be.
几乎所有的测试都会遵循这三个阶段。你不需要像本指南那样用新行将它们分开,但在编写测试时最好记住这三个阶段。
¥Almost all test will follow these three phases. You don't need to separate them with new lines like this guide does, but it is good to keep these three phases in mind as you write your tests.
结论
¥Conclusion
使用
mount()
渲染组件。¥Use
mount()
to render a component.使用
get()
和findAll()
查询 DOM。¥Use
get()
andfindAll()
to query the DOM.trigger()
和setValue()
是模拟用户输入的助手。¥
trigger()
andsetValue()
are helpers to simulate user input.更新 DOM 是一个异步操作,因此请确保使用
async
和await
。¥Updating the DOM is an async operation, so make sure to use
async
andawait
.测试通常分为 3 个阶段;安排、行动和断言。
¥Testing usually consists of 3 phases; arrange, act and assert.