vue测试入门与实践
引言:
在自动化测试中,测试前端应用 一般有3种测试方式 单元测试(unit tests), 快照测试(snapshot tests), 端对端测试(end-to-end tests). 这里我先简单介绍一下前端的3种测试方式,然后着重讲前端单元测试,在对大型vue项目上进行单元测试时遇到的困难 以及解决方法。
e2e测试
端对端测试是最直接的测试类型,在前端应用中,端对端测试使浏览器自动化,从用户的角度检查应用程序是否正常运行。它验证的是一个完整的操作链是否能够完成。
e2e测试示例
1 | // 计算器用例 |
1 | // nutty管理端的e2e 测试 |
e2e测试(属于黑盒测试的一种) 将应用看成一个整体,站在用户的角度上来测试,模拟用户行为,我们只要关心功能实现,不需要关心内部细节。
e2e测试 比起手动测试节省了大量时间,它解决了之前的系统测试都是手动测试的问题。
e2e测试的问题:
- e2e测试很慢、启动浏览器可能都要花几秒、而且如果有的网站很慢,更会拖累速度。如果全靠e2e测试,那么整个测试可能要几十分钟、甚至数小时。
- e2e测试debug很困难,一般来说抛错后 需要打开浏览器,以重现该错误。这个在本地相对还好,一旦在线上的ci流水线上挂了,那么很难顶。
- e2e测试里 难以避免有外部依赖,这样会导致测试不稳定,很容易存在某个api宕机 导致线上流水线一时半会跑不过的问题。
快照测试
传统的快照测试会在浏览器中启动应用程序,并对呈现的页面进行截图。 他们会将新拍摄的屏幕截图与保存的屏幕截图进行比较,如果存在差异,则会显示错误。 当操作系统或浏览器版本之间的差异导致测试失败(即使快照未更改)时,此类快照测试也会出现问题。
jest测试框架编写快照测试。 Jest快照测试可以比较JavaScript中的任何可序列化的值(指可以转换为字符串然后再转换为值的任何代码),而不是比较屏幕截图。 您可以使用它们来比较Vue组件的DOM输出。
良好的前端测试
一个良好的前端测试,一般是由3种测试按一定比例混合而成的。 这是vue测试开发者推荐用的比例。
避免不稳定测试的最好方法是不编写测试,因此只包含一部分e2e测试。
写测试的流程:
- 根据业务线需求 确定需要的组件
- 编写每个组件的源码和单元测试
- 设计组件的样式
- 为完成的组件添加快照测试
- 在浏览器中手动测试
- 编写e2e测试
什么是单元测试?
单元测试是软件开发非常基础的一部分。单元测试会封闭执行最小化单元的代码,使得添加新功能和追踪问题更容易。Vue 的单文件组件使得为组件撰写隔离的单元测试这件事更加直接。它会让你更有信心地开发新特性而不破坏现有的实现,并帮助其他开发者理解你的组件的作用。
为什么要写单元测试
组件的单元测试有很多好处:
- 提供描述组件行为的文档
- 节省手动测试的时间
- 减少研发新特性时产生的 bug
- 改进设计
- 促进重构
自动化测试使得大团队中的开发者可以维护复杂的基础代码。
单元测试的要求
可以快速运行
易于理解
单个函数(方法)的圈复杂度 不要太高。。。。
只测试一个独立单元的工作 单元测试不能有外部依赖,保证测试用例稳定。
一般情况下 尽量少使用mount,尤其是子组件存在 副作用的情况下。 avoid deep rendering when there can be side effects
良好的单元测试可以断言组件的最终状态,而无需考虑实现细节。
怎么写单元测试?
单测3步
数据准备 arrange (set up for the test. In our case, we render the component).
触发行为 act (execute actions on the system).
断言输出 assert (ensure the actual result matches your expectations).
测一个vue组件测的是什么?
输入输出
- 输入
- Component props 组件props输入
- User actions (like a button click) 用户行为(例如点击、输入等)
- Vue events (外部传入的事件)
- Data in a Vuex store (store数据)
- Data in route (route数据)
- 本地数据 (web本地储存等)
- 输出
- Emitted events
- External function calls (methods的函数调用)
- UI变化 (UI改变)
- store改变 (store改变)
给定的输入(属性,触发的事件)而非实现来测试输出(通常是呈现的HTML)
举个单元测试例子
1 | // 一个简单的vue组件。 |
1 | // Hellow.vue组件的单元测试 |
安装准备
1 | tnpm i -D @vue/test-utils @tencent/jest-preset-halo vue-jest vue-template-compiler |
在 package.json 中增加单元测试相关配置
1 | // Vue 项目package.json |
编写单测
编写vue单测的思路
先将vue组件隔离挂载,生成一个wrapper实例(先简单理解为一个vue组件实例), 然后对这个vue实例模拟必要的输入,然后断言vue组件的输出。
Vue Test Utils 通过将它们隔离挂载,然后模拟必要的输入 (prop、注入和用户事件) 和对输出 (渲染结果、触发的自定义事件) 的断言来测试 Vue 组件。
@vue/test-utils中一些常见的api
mount, shallowMount
1 | // 被测vue组件 |
1 | /** |
1 | // mount挂载生成的快照 |
Wrapper
一个 Wrapper
是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法。
- vm
Component
(只读):这是该 Vue 实例。你可以通过 wrapper.vm
访问一个实例所有的方法和属性。这只存在于 Vue 组件包裹器或绑定了 Vue 组件包裹器的 HTMLElement 中。
- find
返回一个DOM节点或者vue组件的wapper
- trigger
在wrapper的DOM节点上触发一个指定的事件
- contains
判断是否包含选择的元素或者组件
- exists
断言wrapper是否存在
- setProps
设置vm的props属性
- setData
设置vm的data属性
单测具体编写
1 | import { shallowMount } from '@vue/test-utils'; // 首先必须引入@vue/test-utils |
启动测试
1 | npm run test // 启动jest测试 |
推荐插件
提升效率,每次测试的时候 都要手动运行jest启动命令、 这个插件在ctrl + s(自动保存的时候),自动运行jest用例,过了打√、错了x。
jest:Toggle Coverage Overlay。 能查看哪里还没有被覆盖。
怎么在chrome上断点调试jest?
1 | import { shallowMount } from '@vue/test-utils' |
然后打开chrome://inspect。
必须点击Resume
单元测试的难点及问题
环境准备(挂载vue实例时的问题)。
vue项目本身就是一个整体 而单元测试,将一个组件从整体里抽离出来后 让它能够运行(也就是环境准备),这部分最难的。
一个vue组件一般会有很多依赖,这些依赖我们要么在shallowMount的时候 将依赖的实例传入,要么mock掉。总而言之不能让vue组件因为缺少这些依赖无法正常运行。
在vue项目中非常容易遇到的一些 环境准备问题及解决方案。
别名无法识别
vue项目中经常有配置别名、而在单测代码中是无法读取到别名的。
1 | // webpack.base.config.js |
1 | // MessageList.vue |
别名无法识别解决方案:在package.json jest中配置别名。
1 | "jest": { |
element-ui未注册
一个大型vue项目,难以避免用了各种ui组件,而且这种ui组件一般都是全局注册的,我们进行单个组件的测试时,是无法访问全局的ui组件的,那么这个时候 据需要在单测代码中 进行ui组件注册或者mock了。
1 | <template> |
1 | import Vue from 'vue'; |
如果是比较复杂的vue组件在挂载时 还能遇到router未定义、vuex未定义、全局变量未定义。。。这些放在后面的怎么测vue逻辑里面。
怎么测试vue组件ui
快照测试
前端组件的输出一般输出的是html字符串,所以ui测试一般用快照测试。
1 | // 环境准备 |
每次修改ui后,快照都会改变,所以要手动更新快照。 当然如果装了jest vs code插件,vs code会提示开发者更新。
1 | // 手动更新快照 运行这个命令,更新快照。 |
判断html是否按预期渲染
1 | // Hello.vue的单测示例 |
怎么测试vue组件逻辑
vue组件逻辑包含 methods,自定义事件、vuex、vue-router等
测试vue methods
建议采用模拟用户交互的形式 进行调用methods。
1 | it('测试msg-btn 按钮点击', () => { |
这里两种方法测的东西都一样,不过前者是通过模拟用户店里按钮 然后调用onClick方法,后面这种是直接通过wrapper实例调用的onClick方法。
我比较推荐前者的写法,模拟用户行为的输入,这才是这个vue组件原有的输入。
测试computed
直接调用 computed方法和method方法一样 都可以通过wrapper.vm方法名 进行调用。
1 | computed: { |
测试抛错
需要在断言里用一个箭头函数包起来,返回一个错误对象,直接对抛错进行断言 会导致测试用例失败。
1 | // 被测函数 |
测试异步
vue组件内的异步操作 主要包括,方法里的ajax请求或其他异步函数 还有dom更新。
测试ajax
ajax请求必须被mock,单元测试不能有外部依赖, 不然这个测试就是不稳定的测试,一旦依赖的后台服务宕机、 那么这个单测就无法通过。
1 | // 被测组件 加载时 发起ajax请求。 |
1 | import 'regenerator-runtime'; // 必须要引入,不然在测试代码中 跑异步操作, 会报错。 vscode会自动引入。 但是请注意lint规范 no-unused-vars。 |
axios-mock-adapter 极力推荐这个moc库,这个还可mock networkError。 mock network timeout等。
非不得已 不要手动去mock axios。
dom更新
Vue 会异步的将未生效的 DOM 批量更新,避免因数据反复变化而导致不必要的渲染。
其中一种办法是使用 await Vue.nextTick()
,一个更简单且清晰的方式则是 await
那个你变更状态的方法,例如 trigger
。
1 | dom 发生更新 vue这个操作是异步的 所以断言dom的变化要等待m更新完毕。 |
可以被 await 的方法有:
测试自定义事件
一般而言自定事件的常见应用是在组件与组件共享数据的时候使用。子组件发出事件,父组件监听事件进行响应。
进行自定义事件的测试过程中,依赖到了两个组件。这种依赖多个组件的测试方式是不提倡的。
一般而言,对emit事件的(子)组件的进行断言就行。wrapper提供了一个emitted方法,可以捕获到该组件发出的所有事件。
1 | // App.vue |
1 | import App from '../App.vue' |
这个是一 bad case。为了测试一个custom event。引入了两个组件。违反了单元测试 只测试一个独立单元的工作 规范。
good case
1 | // MyLogin.vue |
1 | import MyLogin from "@/components/MyLogin.vue" |
- emitted
返回一个包含由 Wrapper
vm
触发的自定义事件的对象。
返回值:{ [name: string]: Array> }
挂载在根Vue原型上的方法或属性。
一般的方法或属性 我推荐在根vue对象上再挂载一遍。
1 | // 被测vue组件 |
尽量少往 Vue根实例上挂载属性或方法,然后在子组件调用,这种很难测。
测试vuex
当挂载组件的时候,组件内声明周期构造里执行了 vuex相关操作,会报错。
我们必须在挂载组件的时候 注入一个store。
站在测试的角度,我们不关心这个 action 做了什么或者这个 store 是什么样子的。我们只需要知道这些 action 将会在适当的时机触发,以及它们触发时的预期值。我们可以把 store 传递给一个 localVue
,而不是传递给基础的 Vue 构造函数。
createLocalVue
返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类。
localVue
是一个独立作用域的 Vue 构造函数,我们可以对其进行改动而不会影响到全局的 Vue 构造函数。
1 | import Vue from 'vue'; |
这样一个store的数据 在测试用例里面会相互依赖,不太好。
还有一种方法,重构store。
1 | // store.js |
Vuex里面的方法,应该最好单独拿出来测试。
1 | describe('mutations测试', () => { |
ps: vuex里不要有存在那种 没有变更状态的操作,列如action里 并没有改变state的操作,没有改变state的action不能被断言。
测试vue路由
在挂载组件的时候,没有router的话,在组件内声明周期内如果有 有关路由的操作(router-view、编程导航等) 也会抛错,导致测试失败中断。
1 | // Home.vue |
1 | import Vue from 'vue'; |
在测试中,你应该杜绝在基本的 Vue 构造函数中安装 Vue Router。安装 Vue Router 之后 Vue 的原型上会增加 $route
和 $router
这两个只读属性。
安装 Vue Router 会在 Vue 的原型上添加 $route
和 $router
只读属性。
这意味着在未来的任何测试中,伪造 $route
或 $router
都会失效。
1 | const wrapper = shallowMount(pageSpec, { |
所以不能修改 $route属性
因为路由需要在应用的全局结构中进行定义,且引入了很多组件,所以最好集成到 end-to-end 测试。对于依赖 vue-router
功能的独立的组件来说,你可以使用mocks 仿造它们。
1 | import { mount } from '@vue/test-utils' |
最佳实践
工厂函数初始化数据
1 | import { shallowMount } from '@vue/test-utils' |
工厂函数的目的就是为了保证 数据不会相互依赖。
1 | // 比较好的工厂函数实践。 |
如何快速的编写单测
TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。 TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。这样的好处是 迫使开发者在编写代码前 要考虑组件设计。前端单元测试一般不会严格遵循TDD,一般是编写代码后 再加端对端和快照测试
当然 我们这边其实 感觉。。。 更多的是先写代码,然后再写测试,甚至有可能是代码已经写了很久了,已经忘了怎么写了 然后再补充单元测试。。。 甚至还有可能是这个代码不是你写的 然后你要给这个业务代码 补充测试。。。
那么怎么快速的编写这种单测呢?
举个栗子
1 | <template> |
这个vue组件 编写单测的难点在哪? 在于输入 在于componentList这个的数据结构是什么,所以只要构造了componentList的数据结构 基本上就测完了。 从这个函数的逻辑 我能预测到这个componentList的数据不会小,所以最好不要去直接写这种mock的入参,尤其是你不知道或者忘记这玩意到底结构是怎么样的情况下。
而这个组件已经在组卡管理端上使用了,所以最快的方法就是去组卡管理端上打断点调试,或者直接打console.log(JSON.stringify(this.componentList))打印出数据, 然后把他格式化。
什么样的vue组件是不好测的vue组件。
1 | // NumStepper.vue |
NumStepper.vue 测试起来非常复杂,因为它关联了外部组件的实现细节。
测试场景中需要一个额外的 NumberDisplay.vue 组件,用来重现外部组件、向目标组件传递数据和方法,并检验目标组件是否正确修改了外部组件的状态。
不难想象,假如 NumberDisplay.vue组件再依赖其他组件或环境变量、全局方法等,事情将变得更糟糕,可能需要单独实现若干测试专用组件,甚至根本无法测试。
测试vue3组件
使用Composition API测试组件构建应该与测试标准组件没有什么不同,因为我们不是在测试实现,而是在测试输出(该组件做什么,而不是它如何执行)。 展示一个使用Vue 2中的Composition API的组件的简单示例,以及测试策略与任何其他组件的相同之处。
1 | <template> |
1 | import { mount } from "@vue/test-utils" |
输入props,输出html。
点击按钮, html count变化。
如果只断言 组件的最终状态,不去测试具体实现。那么vue2的测试对vue3也是可以直接用的。
Reference
Vue 组件的单元测试 官方文档
Testing Vue.js Applications vue-utils的开发者写的书