实例展示vue单元测试及难题解惑

通过生动详实的例子带你排遍vue单元测试过程中的所有疑惑与难题。

技术栈:jest、vue-test-utils。

共四个部分:运行时、Mock、Stub、Configuring和CLI。

运行时

在跑测试用例时,大家的第一个绊脚石肯定是各种undifned报错。

解决这些报错的血泪史还历历在目,现在总结来看,大都是缺少运行时变量抑或异步造成的。

这里咱们只说运行时,基本就这两类:

1.缺少window等环境变量

一般通过引入global-jsdom解决,这也是官方推荐的。当然我们也可以自己在测试代码中直接声明定义。

比如我们在业务代码中使用了sessionStorage。

// procudtpay.vue
<script>
const sessionParams = window.sessionStorage.getItem('sessionParams')
export default {data () { }
}
</script>

然后在测试代码中直接重定义,这样在运行时,实际取到的值就是我们在这里定义的。

// procudtpay.spec.js
window.sessionStorage = {getItem: () => {return { name:'name', type:'type' }}
}
import procudtpay from '../views/procudtpay.vue'

这里关于执行顺序做一点额外说明:

示例中sessionParams的赋值是在import引入.vue模块就执行了的,所以对sessionStorage的定义赋值需要在引入之前。

如果你的sessionStorage取值是在vue实例化后,比如created中,那么则没有该问题。

2.缺少在main.js中定义/注册的全局属性和方法

这些就需要在测试代码中引入同款,以及通过mount的配置项mocks和stubs,分别对其进行mock或者存根了。

// main.js
import Vue from 'vue'
import Mint from 'mint-ui'
import '../filter'
import axios from 'axios'
Vue.use(Mint)
Vue.prototype.$post = (url, params) => {return axios.post(url, params).then(res => res.data)
}
Vue.filter('filterxxx', function (value) {// bala bala ba…
})// xxx.spec.js
import Vue from 'vue'
import '../../filter/filter'   // 引入注册同款过滤器
Vue.filter('filterxxx', function (value) {// bala bala ba…
})
import { $post } from './http.js' 
it('快照测试', () => {const wrapper = shallowMount(ProductPay, {mocks: {$post  // 用自己定义的mock数据取代真实http请求},stubs:['mt-header'] // 存根组件})// ...
})

通常其他测试文件也会依赖这些全局变量,我们可以通过配置jest的setupFiles实现复用。

Mock

我翻开代码一看,这代码没有注释,歪歪斜斜的每一行都写着‘断言正确’四个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满屏都写着两个字:“造假”!

正应了那一句:人(ce)生(shi)如戏,全靠演技(mock)。总之,mock老重要了。

1.mock简单函数

我们从最简单的mock一个函数开始。

比如我们现在想要测试:当用户购买成功,期望页面能跳转到结果页。

// productpay.vue
<script>
export default {...methods:{commmit () {this.$post('xxx', params).then(data => {this.$router.push(`/payresult`)})}}
}
</script>

那么,我们可以通过mock掉$router的push方法,然后断言它有被调用且参数正确,达成测试目的。

// productpay.spec.js
it('当用户购买成功后,页面应该跳转至结果页', async () => {const mockFunc = jest.fn()const wrapper = shallowMount(ProductPay, {mocks: {$post,$router: {push: mockFunc}}})wrapper.vm.commmit() // 提交购买expect(mockFunc).toHaveBeenCalledWith('/payresult')
})

2.mockHttp请求,指定返回结果

http请求和上面例子中的$router的区别是,它需要返回值。jest有多种方式指定返回值,这里用的是mockImplementation。

// test/**.spec.js
it('当用户xxxx,应该xxxx', async () => {const respSuccess = { data: [...], code:0 }const respError = { data: [...], code:888 }// 定义mock函数const mockPost = jest.fn() const wrapper = shallowMount(index, { mocks: {$post:mockPost // 应用该mock函数}})// 指定异步返回数据mockPost.mockImplementation(() => Promise.resolve(respError))// 可以对调用情况进行断言expect(mockPost).toHaveBeenCalled() mockPost.mockImplementation(() => Promise.resolve(respSuccess))//也可以等待异步结束,对结果进行断言await flushPromises()expect(wrapper.vm.list).toEqual(respSuccess.data)
})

实际上我们项目中调用的接口会很多,且不乏返回大量数据的情况。如果这些都定义在测试代码里就会很臃肿。这时候,我们可以对该功能做个简单的模块化。

// 常见的业务代码
// main.js中把axios挂载到了vue实例
Vue.prototype.$post = (url, params) => {return axios.post(url, params).then(res => res.data)
}
// Index.vue中的请求
getProductList () {this.$post('/ProductListQry', {}).then(data => {this.ProductList = data.List})
}
// 1. 在单独js中存放模拟数据 data/ProductListQry.js
export default {data:[{ id:1,name:'name',...},...],code:0
}// 2. 定义post方法,并做个数据匹配 test/http.js
import ProductListQry from '@/data/ProductListQry.js'
const mockData = {ProductListQry,... //可以用同样的方式引入更多mock数据
}
const $post = (url = '') => {return new Promise((resolve, reject) => {const jsName = String(url).split('/')[1]resolve(mockData[jsName])})
}
export { $post }// 3. 引入并使用 test/index.spec.js
import Index from '@/views/Index.vue'
import { $post } from './http.js'
it('...',()=>{const wrapper = shallowMount(Index, {mocks: {$post}})wrapper.vm.getProductList() //触发请求await flushPromises() //等待异步请求结束//可以看到wrapper中就有了我们指定的模拟数据console.log(wrapper.vm.ProductList) 
})

同理,如果要测试请求失败的情形,可以再定义一个返回错误数据的方法,比如就叫$postError。

// test/**.spec.js
import { $postError } from './http.js'
it('...',()=>{const wrapper = shallowMount(Index, {mocks: {$post:$postError}})wrapper.vm.getProductList() //触发请求await flushPromises() //等待异步请求结束// 我们就可以就获取到错误数据的场景进行测试了console.log(wrapper.vm.ProductList) 
})

3.mock整个模块

当业务代码中直接使用了引入的组件/方法时,我们对其测试可能就需要mock整个模块。下面是一个用弹窗做表单验证的场景:

// productpay.vue
<script>
import { MessageBox } from '../Component'
export default {methods:{makeSurebuy () {let payAmount = delcommafy(this.payAmount)if (!payAmount) {MessageBox({message: '请先输入购买金额'})return}if (payAmount < this.resData.BaseAmt) {MessageBox({message: '购买金额不能小于起存金额'})return}if (payAmount > this.Balance) {MessageBox({message: '购买金额不能大于可用余额'})return}// 校验通过,发起交易...}}
}
<script>

//productpay.spce.js
import Component from '../Component'
jest.mock('../../../components/ZyComponent')it('当用户点击购买按钮,如果输入非法金额,应该有相应的错误提示', async () => {wrapper.findAll('.btn-commit').at(0).trigger('click')expect(Component.MessageBox.mock.calls[0][0]).toEqual({ message: '请先输入购买金额' })wrapper.setData({payAmount: '100'})wrapper.findAll('.btn-commit').at(0).trigger('click')expect(Component.MessageBox.mock.calls[1][0]).toEqual({ message: '购买金额不能小于起存金额' })wrapper.setData({payAmount: '100000000000000000'})wrapper.findAll('.btn-commit').at(0).trigger('click')expect(Component.MessageBox.mock.calls[2][0]).toEqual({ message: '购买金额不能大于可用余额' })
})

我们通过jest.mock()mock整个模块,当该模块的方法被调用后它就会有一个mock属性,可以通过ZyComponent.ZyMessageBox.mock进行访问,其中ZyComponent.ZyMessageBox.mock.calls会返回被调用情况的数组,我们可以根据这个数据对函数被调用次数、入参情况进行断言测试。

Stub存根组件

进行单元测试,理论上我们不用、也不应该在它的测试用例中测试子组件,不然就叫集成测试了。vue-test-utils是通过配置stubs实现对组件mock的。

const wrapper = shallowMount(index, {stubs: ['mt-header', 'mt-loadmore']
}

但是业务中难免会有调用子组件方法的时候,比如说mint-ui的loadmore。

// procuctlist.vue
<script>
export default {...methods:{getProductList () {this.$post('xxx', params).then(data => {...this.ProductList = this.ProductList.concat(data.List)this.$refs.loadmore.onBottomLoaded()})}}
}
</script>

这时候我们是可以改用mount方法使页面渲染子组件,这样通过$refs就能正常的获取到子组件实例。但更合适的做法应该是自定义存根组件的内部实现,以满足测试需求。

// procuctlist.spec.js
it('当用户上拉产品列表,应该能看到的更多的产品', () => {const mockOnBottomLoaded = jest.fn()const mtLoadMore = {render: () => { },methods: {onBottomLoaded: mockOnBottomLoaded}}const mtHeader = {render: () => { }}const wrapper = shallowMount(Index, {stubs: { 'mt-loadmore': mtLoadMore, 'mt-header': mtHeader },mocks: {$post}})const currentPage = wrapper.vm.currentPagewrapper.vm.loadMoreProduction()expect(wrapper.vm.currentPage).toEqual(currentPage + 1)expect(mockOnBottomLoaded).toHaveBeenCalled()
})

最后提一嘴,存根组件后,业务代码中子组件还是会被引入的,只是没有被实例化和渲染。

Configuring和CLI

1.统计代码覆盖率忽略某些文件

使用coveragePathIgnorePatterns配置即可,把这个列出来是应为我遇到两个项目相同配置,有一个死活不生效的问题。最后才从官方文档中得知是babel插件istanbul问题。目前还未解决,只是粗暴的在.balelrc中把istanbul去掉了。有真正解决方案的大佬,留言教下……跪谢。

// jest.config.js
{coveragePathIgnorePatterns: ['<rootDir>/src/assets/']
}

2.通过t模式,可以仅执行指定的测试用例

当测试用例写的多了,每次执行跑一堆用例,效率很低,如果代码里有很多console,那就更难受了,找个报错都能找半天。当时就想如果能仅测试当前用例就好了。

然后就找到了t模式,jest命令带–watch参数进入监听模式,然后输入t,再输入匹配规则即可。世界一下子就清净了,舒服……

// package.json
{"scripts":{"tets":"jest --watch"}
}

3.vue-awesome-swiper测试运行时报错

如果组件中引入了swiper,那么在执行测试用例时,vue-awesome-swiper中的js会报错,引用即报错,且是第三方代码。

最后通过把swiper组件由局部注册改为全局注册得以解决。

行动吧,在路上总比一直观望的要好,未来的你肯定会感 谢现在拼搏的自己!如果想学习提升找不到资料,没人答疑解惑时,请及时加入扣群: 320231853,里面有各种软件测试+开发资料和技术可以一起交流学习哦。

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/13739.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【HarmonyOS4学习笔记】《HarmonyOS4+NEXT星河版入门到企业级实战教程》课程学习笔记(九)

课程地址&#xff1a; 黑马程序员HarmonyOS4NEXT星河版入门到企业级实战教程&#xff0c;一套精通鸿蒙应用开发 &#xff08;本篇笔记对应课程第 16 节&#xff09; P16《15.ArkUI-状态管理-任务统计案例》 1、实现任务进度卡片 怎么让进度条和进度展示文本堆叠展示&#xff1…

./scripts/Makefile.clean 文件分析

文章目录 目标 $(subdir-ymn)目标__clean $(clean-dirs):     make -f ./scripts/Makefile.clean obj$(patsubst _clean_%,%,$) $(clean-dirs)$(patsubst _clean_%,%,$)_clean_api _clean_cmd _clean_common _clean_disk _clean_drivers _clean_drivers/ddr/altera _clean_d…

react中的useEffect()的使用

useEffect()是react中的hook函数&#xff0c;作用是用于创建由渲染本身引起的操作&#xff0c;而不是事件的触发&#xff0c;比如Ajax请求&#xff0c;DOM的更改 首先useEffect()是个函数&#xff0c;接受两个参数&#xff0c;第一个参数是一个方法&#xff0c;第二个参数是数…

vue3 路由跳转 携带参数

实现功能&#xff1a;页面A 跳转到 页面B&#xff0c;携带参数 路由router.ts import { createRouter, createWebHistory } from "vue-router";const routes: RouteRecordRaw[] [{path: "/demo/a",name: "aa",component: () > import(&quo…

x264 码率控制原理:x264_ratecontrol_start 函数

x264_ratecontrol_start 函数 函数原理 函数功能:编码一帧之前,为当前帧选择一个量化 QP,属于帧级别码率控制;这对于控制视频质量和文件大小至关重要。通过调整QP,编码器可以在保持视频质量的同时,尽可能减小输出文件的大小。函数参数:x264_t *h: 编码器上下文结构体指…

地信遥感测绘电子书

《地理信息系统概论》&#xff0c;黄杏元&#xff0c;马劲松编著&#xff0c;第三版&#xff0c;高等教育出版社&#xff0c;2008年 《地理信息系统》&#xff08;第二版&#xff09;汤国安&#xff0c;赵牡丹&#xff0c;杨昕等编&#xff0c;高等教育出版社&#xff0c;2010…

【stm32/CubeMX、HAL库】嵌入式实验五:定时器(2)|PWM输出

参考&#xff1a; 【【正点原子】手把手教你学STM32CubeIDE开发】 https://www.bilibili.com/video/BV1Wp42127Cx/?p13&share_sourcecopy_web&vd_source9332b8fc5ea8d349a54c3989f6189fd3 《嵌入式系统基础与实践》刘黎明等编著&#xff0c;第九章定时器&#xff0c…

8操作系统定义、分类及功能+设备管理+作业管理 软设刷题 软考+

操作系统定义、分类及功能设备管理作业管理 知识点1-55-1010-1515-2020-2525-3030-35 刷题操作系统定义、分类及功能1-55-1010-15作业管理1-5设备管理1-55-10 知识点 1-5 1 嵌入式操作系统的特点&#xff1a; 1.微型化&#xff0c;从性能和成本角度考虑&#xff0c;希望占用的…

145.栈和队列:删除字符串中的所有相邻重复项(力扣)

题目描述 代码解决 class Solution { public:string removeDuplicates(string s) {// 定义一个栈来存储字符stack<char> st;// 遍历字符串中的每一个字符for(int i 0; i < s.size(); i){// 如果栈为空或栈顶字符与当前字符不相同&#xff0c;则将当前字符入栈if(st.e…

Jenkins的Pipeline流水线

目录 前言 流水线概念 什么是流水线 Jenkins流水线 pipeline node stage step 创建一个简单的流水线 创建Pipeline项目 选择模板 测试 前言 提到 CI 工具&#xff0c;首先想到的就是“CI 界”的大佬——Jenkjns,虽然在云原生爆发的年代,蹦出来了很多云原生的 CI 工具…

Hive的窗口函数

定义&#xff1a; 聚合函数是针对定义的行集(组)执行聚集,每组只返回一个值.如sum()、avg()、max() 窗口函数也是针对定义的行集(组)执行聚集,可为每组返回多个值.如既要显示聚集前的数据,又要显示聚集后的数据.步骤&#xff1a; 1.将记录分割成多个分区. 2.在各个分区上调用窗…

word-表格疑难杂症诊治

一、用表格进行排版图片、制作公文头 可以在插入图片时固定列宽 二、表格中的疑难杂症 问题一&#xff1a;表格超过页面&#xff0c;右侧文字看不见 解决&#xff1a;表格窗口-布局-自动调整-根据窗口自动调整表格 问题二&#xff1a;表格底部文字被遮挡 解决&#xff1a;布…

【linux】yumvim工具理解使用

目录 Linux 软件包管理器 yum 关于 rzsz 注意事项 查看软件包 Linux开发工具 Linux编辑器-vim使用 vim的基本概念 vim的基本操作 vim正常模式命令集 vim末行模式命令集 简单vim配置 配置文件的位置 sudo提权 Linux 软件包管理器 yum 1.yum是什么&#xff1…

攻防世界---web---warmup

1、题目描述 2、查看源码&#xff0c;发现有个source.php 3、访问该文件&#xff0c;得到这一串代码 4、分析代码 5、访问hint.php&#xff0c;提示flag在ffffllllaaaagggg这个文件下 6、构造payload ?filesource.php?/../../../../../../ffffllllaaaagggg

Fitting Parameterized Three-Dimensional Models to Images

摘要 基于模型的识别和运动跟踪依赖于解决投影和模型参数&#xff0c;使其最佳适应匹配的2D图像特征的3D模型的能力。本文将当前的参数求解方法扩展到处理具有任意曲面和任意数量的内部参数&#xff08;表示关节、可变尺寸或表面变形&#xff09;的对象。开发了数值稳定化方法…

懒人网址导航页 search.html SQL注入漏洞复现

0x01 产品简介 懒人网址导航系统是一种智能化的网址导航平台,旨在帮助用户快速找到所需的网址和资源。该系统提供了智能化的网址搜索和推荐功能,能够根据用户的搜索习惯和偏好推荐相关的网址和资源。同时,系统还提供了网址分类、网址收藏和网址分享等功能,方便用户管理和共…

简易Docker磁盘使用面板Doku

这个项目似乎有 1 年多没更新了&#xff0c;最后发布版本的问题也没人修复&#xff0c;所以看看就行&#xff0c;不建议安装 什么是 Doku &#xff1f; Doku 是一个简单、轻量级的基于 Web 的应用程序&#xff0c;允许您以用户友好的方式监控 Docker 磁盘使用情况。Doku 显示 D…

南京沁恒微USB HUB CH334/CH335多种封装规格选择,外围简单,价格还美丽

概述&#xff1a; CH334 和 CH335 是符合 USB2.0 协议规范的 全速&#xff0c;下行端口支持 USB2.0 高速 480Mbps 个 TT 分时调度 4 个下行端口&#xff09;&#xff0c;还支持高性能的 工业级设计&#xff0c;外围精简&#xff0c;可应用于计算机和工控机主板 特点&#xff1…

精品UI响应式视频教程知识付费系统源码在线教育网络课程在线点播可二开分销分站功能

这是一款知识付费平台模板&#xff0c;后台可上传本地视频&#xff0c;批量上传视频连接&#xff0c; 视频后台可设计权限观看&#xff0c;免费试看时间时长&#xff0c;会员等级观看&#xff0c;付费观看等功能&#xff0c; 也带软件app权限下载&#xff0c;帮助知识教育和软件…

域名邮箱是什么?怎么注册公司的域名邮箱?

拥有一个专业、独特的邮箱地址不仅能提升企业形象&#xff0c;还能增强客户信任感。域名邮箱是什么&#xff1f;域名邮箱也称为企业邮箱或定制邮箱&#xff0c;是一种基于企业自主域名设置的电子邮件服务。本文将详细介绍域名邮箱的概念、优势以及如何注册公司的域名邮箱 一、…