vue3单元测试-项目实战

公众号:小博的前端笔记

1、已有项目集成 vitest

  • 第一步安装依赖

    npm install -D vitest

官方提示Vitest 需要 Vite >=v5.0.0 和 Node >=v18.0.0, 但是我们项目vite版本为2.9.9,也能安装成功。

  • 第二步创建文件夹,暂时将其放在src/unitTest目录下
  • 第三步增加执行命令
json 复制代码
{
  "scripts": {
    "test:unit": "vitest"
  }
}
  • 第四步进行运行测试,不需要额外配置,vitetest和vite集成较好且会自动查找项目中*文件名中包含 ".test." 或 ".spec." 。*的文件

并且此时对测试文件进行更改,热更新还是生效的!

如果想在测试期间想要不同的配置,可以:创建 vitest.config.ts文件优先级会最高.

对比jest

若我们使用jest,则需要进行以下步骤才能使用:

安装依赖--->配置jest.config文件-->配置babel--->package.json增加执行命令--->运行测试

不确定是否有热更新

反观vitejest:

安装依赖--->package.json增加执行命令--->运行测试

只需要简单3步就能运行测试了,若无特殊要求不需要创建vitest.config.ts文件。

2、实际功能初步使用

2.1 普通ts/js文件方法测试

以甘特图为例,甘特图中存在嵌套表格,此时的数据结构为树形结构 ,我们将树形结构扁平化进行使用,这样可以避免使用递归组件。

我们对扁平化方法进行单元测试:

  • 再测试文件中引入甘特图的扁平化方法
  • 因为表格会有展开、收缩的功能,所以此扁平化方法也需根据需求进行对应层级的扁平化化

    • 1、mock一个树形结构数据

    • 2、和想要的结果进行对比

markdown 复制代码
    其中第一个测试方法得到的结果应该是正确的,第二个方法应该是错误的来看运行结果:
markdown 复制代码
    标出了测试运行的结果和对比的结果。

    我们改为预期的结果再运行即可测试通过!

   

2.2 vue文件测试环境配置

我们项目都为vue框架项目,所以很多需要测试的方法都是再.vue文件中的,所以需要支持对vue文件的单元测试。

vitest是测试框架本身不支持对vue框架的语法测试,所以我们需要一个能对vue进行测试的程序库Vue Test Utils,此库由官方提供,是一组实用函数,旨在简化 Vue.js 组件的测试。它提供了一些以独立方式挂载和与 Vue 组件交互的方法。

可以将其与任何测试运行器一起使用.

官方地址:test-utils.vuejs.org/zh/guide/

安装
css 复制代码
npm install --save-dev @vue/test-utils
运行demo

出现报错:properlyReferenceError: document is not defined

1. 问题原因

  • 测试框架(如 Jest/Vitest)默认在 Node.js 环境运行,没有浏览器 DOM(如 document 对象)
  • 当组件或测试代码直接操作 DOM 时,Node.js 环境会报错

2. 解决方案

  • 下载jsdom依赖包并选择次环境

    css 复制代码
      npm install --save-dev jsdom
  • 增加vitestconfigts配置文件,并根据项目已有viteconfigts文件进行合并改动,如下

  • 改完后成功测试vue组件

2.3 官方ToDoApp得到的注意事项

test-utils.vuejs.org/zh/guide/es...

mount说明事项:

  • 这是在 VTU 中渲染组件的主要方式。
  • 调用 mount 并将组件 作为第一个参数传入------这是几乎每个测试都会执行的操作。
  • 将结果赋值给一个名为 wrapper 的变量,因为 mount 提供了一个简单的"包装器",它为测试提供了一些方便的方法。

同步异步事项:

  • Jest 以同步方式执行测试,且在最后一个函数调用后立即结束测试。
  • Vue 会异步更新 DOM。所以我们需要将测试标记为 async,并在任何可能导致 DOM 变化的方法上调用 await

测试阶段事项:

  • 测试分为三个不同的阶段:布置 (Arrange)执行 (Act)断言 (Assert)

    • 布置 (Arrange) 阶段,我们为测试设置场景。更复杂的示例可能需要创建 Vuex store 或填充数据库。
    • 执行 (Act) 阶段,我们模拟用户如何与组件或应用程序交互。
    • 断言 (Assert) 阶段,我们对组件的当前状态进行断言。

几乎所有测试都将遵循这三个阶段。

例如:

2.4 wrapper的部分API介绍

API 作用 本质 备注
get() 用于查找存在的元素 使用 querySelector 语法 没有找到匹配选择器的元素,它会抛出错误,测试将会失败。如果找到了元素,get() 返回一个 DOMWrapper
find()和 exists() 用于查找元素 get() 基于元素存在的假设来工作,当元素不存在时会抛出错误。因此,不推荐使用它来断言元素是否存在。为此,我们使用 find()exists()
data 挂载选项覆盖默认值 挂载选项 挂载选项中的 data 会优先于任何默认值。
isVisible() 检查隐藏元素的能力 isVisible() 会检查: 1、元素或其祖先元素是否具有display: nonevisibility: hiddenopacity: 0样式; 2、元素或其祖先是否位于折叠的<details>标签内; 3、元素本身或其祖先元素是否有hiddenattribute; 在以上任一情况下,isVisible() 都会返回 false。

使用data时需注意组合式写法需以下写法(官方给的是声明式写法):

总结

  • 使用 find() 结合 exists() 验证元素是否在于 DOM 中。
  • 如果你确认元素存在于 DOM 中,就使用 get()
  • 可以使用 data 挂载选项设置组件的默认值。
  • 使用 get()isVisible() 验证在 DOM 中元素的可见性。

3、编写易于测试的组件

3.1、不要测试实现细节

从用户的角度考虑输入和输出。以下大致是在为 Vue 组件编写测试时应当考虑的所有内容:

输入 示例
交互 点击、输入......及任何"人类"交互
Props 组件接收的参数
数据流 从 API 调用、数据订阅等中传入的数据
输出 示例
DOM 元素 渲染到文档中的任何可观察节点
事件 触发的事件(使用 $emit
副作用 console.log 或 API 调用

其他一切都是实现细节

注意,这个列表不包含内部方法、中间状态或甚至数据等元素。

经验法则是测试在重构时不应失败 ,也就是说,当我们更改其内部实现而不改变其行为 时,测试不应失败。如果发生这种情况,则测试可能依赖于实现细节。

例如,假设有一个简单的计数器组件,包含一个增加计数的按钮:

xml 复制代码
<!-- Counter.vue -->
<script setup>
import { ref } from 'vue'
​
const count = ref(0)
​
const increment = () => {
  count.value++
}
</script>
​
<template>
  <p class="paragraph">Times clicked: {{ count }}</p>
  <button @click="increment">Increment</button>
</template>

我们可以编写以下测试:

javascript 复制代码
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
​
test('counter text updates', async () => {
  const wrapper = mount(Counter)
  const paragraph = wrapper.find('.paragraph')
​
  expect(paragraph.text()).toBe('Times clicked: 0')
​
  await wrapper.setData({ count: 2 })
​
  expect(paragraph.text()).toBe('Times clicked: 2')
})

注意这里我们在更新其内部数据,并且也依赖于用户视角的细节。

注意,更改数据或 CSS 类名都会导致测试失败。然而,组件仍然可以按预期工作。这种情况称为误报 (false positive)

相反,以下测试尽量使用前文提到的输入和输出:

javascript 复制代码
import { mount } from '@vue/test-utils'
​
test('text updates on clicking', async () => {
  const wrapper = mount(Counter)
​
  expect(wrapper.text()).toContain('Times clicked: 0')
​
  const button = wrapper.find('button')
  await button.trigger('click')
  await button.trigger('click')
​
  expect(wrapper.text()).toContain('Times clicked: 2')
})

Vue Testing Library 这样的库就是基于这些原则构建的。

3.2、构建更小、更简单的组件

有时,一个组件可能包含复杂的方法、密集的计算或使用多个依赖项。

这里的建议是提取该方法并将其导入组件。这样,你可以使用 Jest 或其他测试运行器独立测试该方法。

此外,如果这个复杂方法难以调试或速度较慢,你可能希望对其进行模拟,以使测试更简单和更快速。发起 HTTP 请求中的示例就是一个很好的例子------axios 是一个很复杂的库!

3.3、在编写组件之前编写测试

如果你提前编写测试,就不会写出不可测试的代码!

4、 实际vue文件功能测试

4.1、测试计划管理快速筛选功能(http模拟数据输入+dom显示)

  • 测试目的:

    • 快速搜索组件显示的人员是否正确
  • 测试思路:

    • 我们的第一个目标是测试这个组件而不实际访问 API。这样会导致测试变得不稳定且可能执行缓慢。
    • 其次,我们需要断言组件是否以正确的参数进行了正确的调用。虽然我们不会从 API 获取结果,但仍需确保请求了正确的资源。
    • 此外,我们还需要确保 DOM 已相应更新并显示数据。我们可以使用 @vue/test-utils 中的 flushPromises() 函数来实现。
  • 具体实现:

    • 运行后报错如下:

虽然此文件是在vite.config中引入的,但是此文件是全局的所以测试文件也会引入,但是此时的路径会相对于测试文件,所以我们将路径改为绝对路径

继续运行报错如下:

经各种查询得知被测试的组件需要挂载所需的三方库,比如element、pinia、i18n等等,所以进行以下处理

继续运行,报错如下:

原因没有相关缓存。。。。,其实此时我们可以对store做单独的处理*(mock store)*使其处于一个干净的环境,此处先不处理,直接对语法进行兼容。

继续运行,报错如下:

原因: ResizeObserver 是浏览器环境中的 API,而 Vitest 默认在 Node.js 环境中运行测试时无法识别它,虽然我们配置了jsdom环境,但是可能是jsdom也没有包含ResizeObserver ,、

解决:1、模拟ResizeObserver ;2、下载三方库, 我们选择1

新建setup.ts文件,模拟ResizeObserver ,如下图:

继续运行后报错:

原因: 经过排查,

此代码未生效,因为vue-utils给的是jest的实例,毕竟vitest和jest还是有区别的,所以重新整理我们目的是拦截http请求并返回模拟数据,所以经过多次询问D哥,了解到两个模拟库:

MSW

复制代码
#### **选择 MSW 的场景**

* **需要全面拦截网络请求** :例如项目中同时使用 `axios`、`fetch` 或其他 HTTP 客户端。
* **集成测试或端到端测试**:希望模拟真实网络环境,验证请求与响应的完整流程。
* **无侵入性需求**:不想在业务代码中引入任何测试逻辑,保持代码纯净。
* **复杂场景模拟** :如模拟请求延迟(`delay`)、动态响应生成、错误重试等。
* **示例代码(MSW 配置)** :
*

  ```javascript
    import { setupWorker, rest } from 'msw'
    ​
    const worker = setupWorker(
      rest.get('/api/data', (req, res, ctx) => {
        return res(ctx.json({ data: 'mocked' }))
      })
    )
    ​
    worker.start()
  ```

axios-mock-adapter

复制代码
#### **选择 axios-mock-adapter 的场景**
  • 仅需模拟 axios 请求:项目完全依赖 axios,无需处理其他 HTTP 客户端。
  • 单元测试轻量化:快速为 axios 实例配置 mock 响应,适合单一功能模块测试。
  • 简单错误模拟:例如直接返回 500 错误或特定响应状态码。
  • 示例代码(axios-mock-adapter 配置)
复制代码
```javascript
  import axios from 'axios'
  import MockAdapter from 'axios-mock-adapter'

  const mock = new MockAdapter(axios)
  mock.onGet('/api/data').reply(200, { data: 'mocked' })
```

我们此时只需要模拟axios即可,不需要跨平台,所以选择axios-mock-adapte

css 复制代码
npm install axios-mock-adapter --save-dev

修改后的代码如下:

运行单元测试发现报错如下:

原因:因为我们改为了类名进行测试,拥有此类型的第一个下标为全部按钮,修改过后成功通过测试!

4.2、总结:

  • 1、组件中的http采用axios-mock-adapter进行模拟即可。

  • 2、组件中使用到的三方库需进行挂载,比如element、pinia、i18n等;

    • 对三方库进行全局挂载,在创建的setup配置文件中
相关推荐
石小石Orz1 小时前
如何将本地文件转成流数据传递给后端?
前端·vue.js
tianzhiyi1989sq2 小时前
Vue框架深度解析:从Vue2到Vue3的技术演进与实践指南
前端·javascript·vue.js
团酱2 小时前
sass-loader与webpack版本冲突解决方案
前端·vue.js·webpack·sass
可可格子衫4 小时前
keep-alive缓存文章列表案例完整代码(Vue2)
vue.js·缓存
洛小豆4 小时前
为什么可以通过域名访问接口,但不能通过IP地址访问接口?
前端·javascript·vue.js
武昌库里写JAVA4 小时前
VUE vuex深入浅出
vue.js·spring boot·毕业设计·layui·课程设计
代码老y4 小时前
Spring Boot + MyBatis + Vue:从零到一构建全栈应用
vue.js·spring boot·mybatis
洛小豆4 小时前
她问我Pinia两种Store定义方式,到底选哪种写法,我说我也不知道...
前端·vue.js·代码规范
罗政4 小时前
小区物业管理系统源码+SpringBoot + Vue (前后端分离)
vue.js·spring boot·后端
ew452184 小时前
【VUE】某时间某空间占用情况效果展示,vue2+element ui实现。场景:会议室占用、教室占用等。
前端·vue.js·ui·elementui