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

news2024/11/18 21:26:22

通过生动详实的例子带你排遍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.currentPage

    wrapper.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.coloradmin.cn/o/1687138.html

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

相关文章

精品干货-阿里巴巴华为等知名大厂数据中台最佳实践方案合集(52份可下载)

【1】关注本公众号 【1】私信发送 数据中台52 获取下载链接。 如需下载本方案PPT/WORD原格式&#xff0c;请加入微信扫描以下方案驿站知识星球&#xff0c;获取上万份PPT/WORD解决方案&#xff01;&#xff01;&#xff01;感谢支持&#xff01;&#xff01;&#xff01;

[STM32-HAL库]0.96寸OLED显示屏-模拟IIC-STM32CUBEMX开发-HAL库开发系列-主控STM32F103C8T6

目录 一、前言 二、详细步骤 1.配置STM32CUBEMX 2.导入OLED库 3.程序设计 3.1 初始化OLED显示屏 3.2 显示字符串 3.3 显示汉字 三、汉字取模 四、总结 一、前言 OLED显示屏是一种十分常见的显示模块&#xff0c;通过用它作为采集到的传感器数据的展示模块。本文通过STM32CU…

每日一题——Python实现PAT甲级1028 List Sorting(举一反三+思想解读+逐步优化)

一个认为一切根源都是“自己不够强”的INTJ 个人主页&#xff1a;用哲学编程-CSDN博客专栏&#xff1a;每日一题——举一反三Python编程学习Python内置函数 Python-3.12.0文档解读 目录 ​编辑​编辑​编辑我的写法&#xff1a; 代码点评 代码点评&#xff1a; 时间复杂…

EasyExcel的CellWriteHandler注入CellStyle不生效

文章目录 一、问题描述二、问题排查三、解决问题 一、问题描述 最近发现原本项目的导出excel功能中&#xff0c;写的那些 CellWriteHandler 去改变样式的代码全都不生效了 二、问题排查 由于代码都是没有改动的&#xff0c;加上最近有升级过 easyExcel 的版本&#xff0c;由…

js实现鼠标拖拽多选功能

实现功能 在PC端的H5页面中&#xff0c;客户拖动鼠标可以连选多个选项 效果展示 具体代码如下 <!DOCTYPE html> <html><head><title>鼠标拖拽多选功能</title><script src"https://cdn.bootcss.com/jquery/1.10.2/jquery.min.js&quo…

ArcGIS基础操作-ArcGIS插值交叉验证方法与流程

ArcGIS基础操作-ArcGIS插值交叉验证方法与流程 交叉验证(Cross-validation)主要用于建模应用中&#xff0c;例如PCR、PLS回归建模中。在给定的建模样本中&#xff0c;拿出大部分样本进行建模型&#xff0c;留小部分样本用刚建立的模型进行预报&#xff0c;并求这小部分样本的预…

自己手写一个字符串【C风格】

//字符串的常见操作 #include <iostream>#define MAX_SIZE 15 #define OK 1 #define ERROR 0 #define TRUE 1 #define FALSE 0 typedef int Status;//状态类型 typedef char ElemType;//元素类型typedef ElemType String[MAX_SIZE 1];//第一个字节记录长度//***tring是数…

华为设备WLAN配置之AP上线

WLAN基础配置之AP上线 配置WLAN无线网络的第一阶段&#xff0c;AP上线技术&#xff1a; 实验目标&#xff1a;使得AP能够获得来自AC的DHCP地址服务的地址&#xff0c;且是该网段地址池中的IP。 实验步骤&#xff1a; 1.把AC当作三层交换机配置虚拟网关 sys Enter system view,…

2024年艺术鉴赏与文化传播国际会议(AACC 2024)

2024年艺术鉴赏与文化传播国际会议&#xff08;AACC 2024&#xff09; 2024 International Conference on Art Appreciation and Cultural Communication 【重要信息】 大会地点&#xff1a;贵阳 大会官网&#xff1a;http://www.icaacc.com 投稿邮箱&#xff1a;icaaccsub-co…

webpack5零基础入门-16封装cssloader函数

1.背景 我们发现配的cssloader中有很多重复性代码&#xff0c;所以应该对其进行封装&#xff0c;减少冗余的代码 2.定义函数getCssLoader function getCssLoader(pre) {return [MiniCssExtractPlugin.loader, css-loader,{loader: postcss-loader,options: {postcssOptions:…

【笔记】树(Tree)

一、树的基本概念 1、树的简介 之前我们都是在谈论一对一的线性数据结构&#xff0c;可现实中也有很多一对多的情况需要处理&#xff0c;所以我们就需要一种能实现一对多的数据结构--“树”。 2、树的定义 树&#xff08;Tree&#xff09;是一种非线性的数据结构&#xff0…

Excel/WPS《超级处理器》同类项处理,合并同类项与拆分同类项目

在工作中处理表格数据&#xff0c;经常会遇到同类项处理的问题&#xff0c;合并同类项或者拆分同类项&#xff0c;接下来介绍使用超级处理器工具如何完成。 合并同类项 将同一列中的相同内容合并为一个单元格。 1&#xff09;用分隔符号隔开 将AB列表格&#xff0c;合并后为…

UML中的图-13中UML图详解

图是一组元素的图形表示&#xff0c;大多数情况下把图画成顶点和弧的联通图。 UML提供了13种图&#xff0c;分别是类图、对象图、用例图、序列图、通信图、状态图、活动图、构建图、组合结构图、部署图、包图、交互概览图和计时图。序列图、通信图、交互概览图和计时图均被称为…

vue3的核心API功能:computed()API使用

常规使用方法: 这样是常规使用方法. 另一种使用方法: 这样分别定义computed的get回调函数和set回调函数, 上面例子定义了plusOne.value的值为1, 那么这时候就走了computed的set回调函数,而没有走get回调函数. 当我们打印plusOne.value的值的时候,走的是get的回调函数而不是…

ubuntu20.04 10分钟搭建无延迟大疆无人机多线程流媒体服务器

1.使用效果 无人机画面 2.服务器视频端口 3.使用教程 3.1.下载ubuntu对应软件包&#xff1a;系统要求ubuntu16以上 3.2修改端口&#xff08;config.xml文件&#xff09; 3.3启动服务 目录下输入&#xff1a;终端启动&#xff1a;./smart_rtmpd 后台启动&#xff1a;nohup ./…

大语言模型的工程技巧(一)——GPU计算

相关说明 这篇文章的大部分内容参考自我的新书《解构大语言模型&#xff1a;从线性回归到通用人工智能》&#xff0c;欢迎有兴趣的读者多多支持。 本文涉及到的代码链接如下&#xff1a;regression2chatgpt/ch07_autograd/gpu.ipynb 本文将讨论如何利用PyTorch实现GPU计算。本…

dubbo复习: (6)和springboot集成时的条件路由

根据指定的条件&#xff0c;对不满足条件的请求进行拦截。 比如拦截ip地址为192.168.31.227的请求。只需要在dubbo admin中的条件路由菜单创建相应的规则 enabled: true force: true runtime: true conditions:- host ! 192.168.31.227

单例模式介绍,及其应用场景?

单例模式(Singleton Pattern)是 Java中最简单的设计模式之一,此模式保证某个类在运行期间,只有一个实例对外提供服务&#xff0c;而这个类被称为单例类。 单例模式也比较好理解&#xff0c;比如一个人一生当中只能有一个真实的身份证号&#xff0c;一个国家只有一个政府&#x…

【linux】深入了解线程池:基本概念与代码实例(C++)

文章目录 1. 前言1.1 概念1.2 应用场景1.3 线程池的种类1.4 线程池的通常组成 2. 代码示例2.1 log.hpp2.2 lockGuard.hpp① pthread_mutex_t 2.3 Task.hpp2.4 thread.hpp2.5 threadPool.hpp① 基本框架② 成员变量③ 构造函数④ 其余功能函数&#xff1a; main.cc结果演示 完整…