发布于 2026-01-06 0 阅读
0

Vue 应用测试入门指南。DEV 全球展示挑战赛,由 Mux 呈现:展示你的项目!

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>
Enter fullscreen mode Exit fullscreen mode

应用简介。

我们正在测试的应用程序是一个 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

Enter fullscreen mode Exit fullscreen mode
  • 安装测试所需的软件包:
    • @vue/test-utilspackage,它是 vuejs 的官方测试库。
    • flush-promises该包是一个简单的包,它会刷新所有待处理的已解决的 Promise 处理程序(我们稍后会详细介绍)。

npm i --save-dev @vue/test-utils flush-promises

Enter fullscreen mode Exit fullscreen mode
  • 我们将为该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
      }
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

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)
  })
})


Enter fullscreen mode Exit fullscreen mode

我们做的第一件事就是从库中导入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)
  })

Enter fullscreen mode Exit fullscreen mode
  • 包装器上的函数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)
  })
})



Enter fullscreen mode Exit fullscreen mode

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)
  })

Enter fullscreen mode Exit fullscreen mode

我们引入了一个新功能——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)
  })


Enter fullscreen mode Exit fullscreen mode

我们做了以下工作:

  • 我们使用其属性找到了输入框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)
  })

Enter fullscreen mode Exit fullscreen mode

现在运行测试应该会成功。太棒了!

文章来源:https://dev.to/bahdcoder/a-gentle-introduction-to-testing-vue-applications-52jk