Vue应用程序测试入门指南。
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
介绍
在本教程中,我们将介绍如何测试 Vue.js 应用程序和组件。我们将测试这个简单的待办事项应用程序。
该应用程序的源代码位于此处。
为了简化操作,本应用程序仅使用一个组件构建App.vue。以下是它的外观:
// src/App.vue
<template>
<div class="container text-center">
<div class="row">
<div class="col-md-8 col-lg-8 offset-lg-2 offset-md-2">
<div class="card mt-5">
<div class="card-body">
<input data-testid="todo-input" @keyup.enter="e => editing ? updateTodo() : saveTodo()" v-model="newTodo" type="text" class="form-control p-3" placeholder="Add new todo ...">
<ul class="list-group" v-if="!editing" data-testid="todos">
<li :data-testid="`todo-${todo.id}`" class="list-group-item" v-for="todo in todos" :key="todo.id">
{{ todo.name }}
<div class="float-right">
<button :data-testid="`edit-button-${todo.id}`" class="btn btn-sm btn-primary mr-2" @click="editTodo(todo)">Edit</button>
<button :data-testid="`delete-button-${todo.id}`" class="btn btn-sm btn-danger" @click="deleteTodo(todo)">Delete</button>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'app',
mounted () {
this.fetchTodos()
},
data () {
return {
todos: [],
newTodo: '',
editing: false,
editingIndex: null,
apiUrl: 'https://5aa775d97f6fcb0014ee249e.mockapi.io'
}
},
methods: {
async saveTodo () {
const { data } = await axios.post(`${this.apiUrl}/todos`, {
name: this.newTodo
})
this.todos.push(data)
this.newTodo = ''
},
async deleteTodo (todo) {
await axios.delete(`${this.apiUrl}/todos/${todo.id}`)
this.todos.splice(this.todos.indexOf(todo), 1)
},
editTodo (todo) {
this.editing = true
this.newTodo = todo.name
this.editingIndex = this.todos.indexOf(todo)
},
async updateTodo () {
const todo = this.todos[this.editingIndex]
const { data } = await axios.put(`${this.apiUrl}/todos/${todo.id}`, {
name: this.newTodo
})
this.newTodo = ''
this.editing = false
this.todos.splice(this.todos.indexOf(todo), 1, data)
},
async fetchTodos () {
const { data } = await axios.get(`${this.apiUrl}/todos`)
this.todos = data
}
}
}
</script>
应用简介。
我们正在测试的应用程序是一个 CRUD 待办事项应用程序。
- 组件挂载时,
fetchTodos会调用一个函数。该函数会调用外部 API 获取待办事项列表。 - 待办事项列表以无序列表的形式显示。
- 每个列表项都有一个动态
data-testid属性,该属性使用待办事项的唯一 ID 生成。稍后我们将在测试中使用此属性。如果您想了解为什么我们使用数据属性而不是传统的类和 ID,请参阅此处。 - 无序列表、输入字段、编辑和删除按钮也具有
data-testid属性。
设置
- 将 GitHub 仓库克隆到本地,并安装所有 npm 依赖项:
git clone https://github.com/bahdcoder/testing-vue-apps
cd testing-vue-apps && npm install
- 安装测试所需的软件包:
@vue/test-utilspackage,它是 vuejs 的官方测试库。flush-promises该包是一个简单的包,它会刷新所有待处理的已解决的 Promise 处理程序(我们稍后会详细介绍)。
npm i --save-dev @vue/test-utils flush-promises
- 我们将为该
axios库创建一个模拟对象,用于我们的测试,因为我们不想在测试期间发出真实的 API 请求。创建一个test/__mocks__/axios.js文件,并将以下模拟对象粘贴到其中:
// __mocks__/axios.js
export default {
async get () {
return {
data: [{
id: 1,
name: 'first todo'
}, {
id: 2,
name: 'second todo'
}]
}
},
async post (path, data) {
return {
data: {
id: 3,
name: data.name
}
}
},
async delete (path) {},
async put (path, data) {
return {
data: {
id: path[path.length - 1],
name: data.name
}
}
}
}
Jest 会自动识别这个文件,并axios在运行测试时将其替换为已安装的库。例如,该get函数返回一个解析后包含两个待办事项的 Promise,每次axios.get在应用程序中调用该函数时,Jest 都会将其功能替换为我们模拟库中的相应功能。
编写我们的第一个测试
在tests/unit目录中,创建一个名为 的新文件app.spec.js,并将以下内容添加到该文件中:
// tests/unit/app.spec.js
import App from '@/App.vue'
import { mount } from '@vue/test-utils'
describe('App.vue', () => {
it('displays a list of todos when component is mounted', () => {
const wrapper = mount(App)
})
})
我们做的第一件事就是从库中导入App.vue组件和mount函数@vue/test-utils。
接下来,我们调用该mount函数,并将App组件作为参数传递。
mount 函数会像在真实浏览器中一样渲染 App 组件,并返回一个包装器。正如我们将在下文看到的,这个包装器包含许多用于测试的辅助函数。
如您所见,我们想要测试从 API 获取待办事项列表,并在组件挂载时将其显示为无序列表。
由于我们已经通过调用该mount函数渲染了组件,我们将搜索列表项,并确保它们显示出来。
// app.spec.js
it('displays a list of todos when component is mounted', () => {
const wrapper = mount(App)
const todosList = wrapper.find('[data-testid="todos"]')
expect(todosList.element.children.length).toBe(2)
})
- 包装器上的函数
find接收CSS selector一个选择器,并使用该选择器在组件中查找元素。
遗憾的是,此时运行此测试会失败,因为断言会在fetchTodos函数解析并返回待办事项列表之前执行。为了确保我们的 axios 模拟对象在断言执行之前解析并返回待办事项列表,我们将按flush-promises如下方式使用我们的库:
// app.spec.js
import App from '@/App.vue'
import { mount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
describe('App.vue', () => {
it('displays a list of todos when component is mounted', async () => {
// Mount the component
const wrapper = mount(App)
// Wait for fetchTodos function to resolve with list of todos
await flushPromises()
// Find the unordered list
const todosList = wrapper.find('[data-testid="todos"]')
// Expect that the unordered list should have two children
expect(todosList.element.children.length).toBe(2)
})
})
该find函数返回一个包装器,我们可以在其中获取实际的值DOM-element,该值保存在element属性中。因此,我们断言子项的数量应该等于 2(因为我们的axios.get模拟返回的是一个包含两个待办事项的数组)。
我们的测试运行通过了。太好了!
测试用户是否可以删除待办事项
每个待办事项都有一个删除按钮,当用户点击此按钮时,应该删除该待办事项并将其从列表中移除。
// app.spec.js
it('deletes a todo and removes it from the list', async () => {
// Mount the component
const wrapper = mount(App)
// wait for the fetchTodos function to resolve with the list of todos.
await flushPromises()
// Find the unordered list and expect that there are two children
expect(wrapper.find('[data-testid="todos"]').element.children.length).toBe(2)
// Find the delete button for the first to-do item and trigger a click event on it.
wrapper.find('[data-testid="delete-button-1"]').trigger('click')
// Wait for the deleteTodo function to resolve.
await flushPromises()
// Find the unordered list and expect that there is only one child
expect(wrapper.find('[data-testid="todos"]').element.children.length).toBe(1)
// expect that the deleted todo does not exist anymore on the list
expect(wrapper.contains(`[data-testid="todo-1"]`)).toBe(false)
})
我们引入了一个新功能——trigger函数。当我们使用该函数找到一个元素时,就可以使用该函数触发该元素的 DOM 事件。例如,我们可以通过调用找到的待办事项元素上的函数find来模拟点击删除按钮。trigger('click')
点击此按钮后,我们将调用该await flushPromises()函数,以便该deleteTodo函数能够执行完毕,之后我们就可以运行我们的断言了。
我们还引入了一个新函数,contains它接受一个元素CSS selector,并根据该元素是否存在于元素中返回一个布尔值DOM。
因此,对于我们的断言,我们断言无序列表中的列表项数量todos为 1,并且最后还断言 DOM 中不包含我们刚刚删除的待办事项的列表项。
测试用户可以创建待办事项
当用户输入新的待办事项并按下回车键时,新的待办事项将被保存到 API 中,并添加到待办事项的无序列表中。
// app.spec.js
it('creates a new todo item', async () => {
const NEW_TODO_TEXT = 'BUY A PAIR OF SHOES FROM THE SHOP'
// mount the App component
const wrapper = mount(App)
// wait for fetchTodos function to resolve
await flushPromises()
// find the input element for creating new todos
const todoInput = wrapper.find('[data-testid="todo-input"]')
// get the element, and set its value to be the new todo text
todoInput.element.value = NEW_TODO_TEXT
// trigger an input event, which will simulate a user typing into the input field.
todoInput.trigger('input')
// hit the enter button to trigger saving a todo
todoInput.trigger('keyup.enter')
// wait for the saveTodo function to resolve
await flushPromises()
// expect the the number of elements in the todos unordered list has increased to 3
expect(wrapper.find('[data-testid="todos"]').element.children.length).toBe(3)
// since our axios.post mock returns a todo with id of 3, we expect to find this element in the DOM, and its text to match the text we put into the input field.
expect(wrapper.find('[data-testid="todo-3"]').text())
.toMatch(NEW_TODO_TEXT)
})
我们做了以下工作:
-
我们使用其属性找到了输入框
data-testid attribute selector,然后将其值设置为NEW_TODO_TEXT字符串常量。通过触发函数,我们触发了input事件,这相当于用户在输入框中输入内容。 -
要保存待办事项,我们按下回车键,触发
keyup.enter事件。接下来,我们调用该flushPromises函数,等待其saveTodo执行完毕。 -
此时,我们运行断言:
- 首先,我们找到无序列表,并期望它现在有三个待办事项:两个来自
fetchTodos组件挂载时调用函数,一个来自创建一个新组件。 - 接下来,我们使用
data-testid,找到刚刚创建的特定待办事项(我们使用 ,todo-3因为我们的函数模拟axios.post返回一个值为 3 的新待办事项id)。 - 我们断言,此列表项中的文本与我们在文本开头输入框中输入的文本相等。
- 请注意,我们使用该
.toMatch()函数是因为此文本还包含“Edit和”Delete文本。
- 首先,我们找到无序列表,并期望它现在有三个待办事项:两个来自
测试用户是否可以更新待办事项
更新过程的测试与我们之前所做的类似。具体如下:
// app.spec.js
it('updates a todo item', async () => {
const UPDATED_TODO_TEXT = 'UPDATED TODO TEXT'
// Mount the component
const wrapper = mount(App)
// Wait for the fetchTodos function to resolve
await flushPromises()
// Simulate a click on the edit button of the first to-do item
wrapper.find('[data-testid="edit-button-1"]').trigger('click')
// make sure the list of todos is hidden after clicking the edit button
expect(wrapper.contains('[data-testid="todos"]')).toBe(false)
// find the todo input
const todoInput = wrapper.find('[data-testid="todo-input"]')
// set its value to be the updated texr
todoInput.element.value = UPDATED_TODO_TEXT
// trigger the input event, similar to typing into the input field
todoInput.trigger('input')
// Trigger the keyup event on the enter button, which will call the updateTodo function
todoInput.trigger('keyup.enter')
// Wait for the updateTodo function to resolve.
await flushPromises()
// Expect that the list of todos is displayed again
expect(wrapper.contains('[data-testid="todos"]')).toBe(true)
// Find the todo with the id of 1 and expect that its text matches the new text we typed in.
expect(wrapper.find('[data-testid="todo-1"]').text()).toMatch(UPDATED_TODO_TEXT)
})
现在运行测试应该会成功。太棒了!
文章来源:https://dev.to/bahdcoder/a-gentle-introduction-to-testing-vue-applications-52jk