您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

基于Vue源码中e2e测试实践

  • 前言
  • 技术选型&对Vue的参考
  • Puppeteer测试流程
  • 在Concis中的实践
  • 项目目录整理
  • Concis组件库

前言

最近半年博主一直在抽空自研一款React组件库Concis,对于测试原本只支持于jest+enzyme单元测试,而单元测试的缺陷是无法模拟用户的一些操作,如表单组件,只能测试到一些组件渲染mount准确性的测试,对于一系列用户操作的模拟测试是无法做到的,因此博主考虑加入e2e测试从而让组件上线后更加健全。

关于单元测试,博主之前整理了一份基于Concis项目自用的总结:

全网最细:Jest+Enzyme测试React组件(包含交互、DOM、样式测试)

技术选型&对Vue的参考

其实博主本身对e2e测试的技术栈比较陌生,于是决定学习一下目前主流框架Vue的e2e测试是如何去做的,学习一下尤大大的技术选型,哈哈哈~

这是github中Vue的test文件夹:

我们进入到e2e中可以看到,圈起来的都是e2e测试用例,而utils应该就是e2e的初始化用于在每一个测试用例文件执行前初始化的,看一下e2eUtils.ts这个文件:

好了,技术选型确认完毕,使用的是puppeteer这个包,大概研究一下使用,这里尤大大其实是基于pupeteer一系列page执行性api进行了二次封装并且最终导出,在每一个测试用例中去使用:


而这些方法博主最后整理了一下,有很详细的备注,具体代码在最后~

Puppeteer测试流程

这里简单介绍一下使用Puppeteere2e测试的整体流程:

  1. 初始化browser实例,构造测试模拟浏览器;
  2. 基于browser实例,新建一个页面实例;
  3. 打开测试url;
  4. 一系列的操作,DOM操作,mock用户;
  5. 关闭页面;
  6. 关闭浏览器;

这里整理了一份入门版简易代码,对照上述流程是这样的:

const testPupeteer = async () => {let brwoser: puppeteer.Browser;let page: puppeteer.Page;brwoser = await puppeteer.launch({                          //打开浏览器headless: false,})await page.goto("http://localhost:8000");                 //page页面跳转测试urlawait page.type('body .testDiv .username', 'username');   //输入用户名await page.type('body .testDiv .password', '123456');     //输入密码await page.click('body .testdiv .login-btn');             //登录
}

可以看到,代码很清晰,由于是真实操作,可能有的操作会涉及网络请求,因此都是异步访问按顺序阻塞执行操作。

代码中模拟了一次用户打开浏览器、打开页面、输入用户名密码、点击登录按钮的一次登录操作,但是pupeteer只提供了模拟操作,并没有对操作进行反馈做判断,这时就需要配合jest的断言来进行e2e整体测试了。

在Concis中的实践

这里博主借鉴了Vue的e2e测试方法,即同样的写了一个setup方法用于初始化pupeteer的起步动作,并且二次封装了一系列方法。

e2eUtils.ts代码如下:

import puppeteer from 'puppeteer';const getExampleUrl = (componentName: string) => {//获取测试组件的页面urlreturn `http://localhost:8000/#/common/${componentName}`;
};
const e2eTestTimeout = 30 * 1000;const setupPuppeteer = () => {let browser: puppeteer.Browser;let page: puppeteer.Page;beforeAll(async () => {browser = await puppeteer.launch({headless: false,});});beforeEach(async () => {page = await browser.newPage();await page.evaluateOnNewDocument(() => {localStorage.clear();});page.on('console', (e) => {if (e.type() === 'error') {const err = e.args()[0];console.error(`Error from Puppeteer-loaded page:\n`, err);}});});afterEach(async () => {await page.close();});afterAll(async () => {await browser.close();});const click = async (dom: string, options?: puppeteer.ClickOptions) => {//点击元素await page.click(dom, options);};const getCount = async (dom: string) => {//获取元素数量return (await page.$$(dom)).length;};const getText = async (dom: string) => {//获取元素文本内容return await page.$eval(dom, (node) => node.textContent);};const getValue = async (dom: string) => {//获取文本框的内容return await page.$eval(dom, (node) => (node as HTMLInputElement).value);};const getHtml = async (dom: string) => {//获取元素的innerHTMLreturn await page.$eval(dom, (node) => node.innerHTML);};const getClassList = async (dom: string) => {//获取元素所有类名return await page.$eval(dom, (node) => [...node.classList]);};const getChildrenCount = async (dom: string) => {//获取子元素数量return await page.$eval(dom, (node) => node.children.length);};const domIsShow = async (dom: string) => {//判断元素是否在document中const display = await page.$eval(dom, (node) => {return window.getComputedStyle(node).display;});return display !== 'none';};const isChecked = async (dom: string) => {//判断多选框是否被选中return await page.$eval(dom, (node) => (node as HTMLInputElement).checked);};const isFocused = async (dom: string) => {//判断元素是否被聚焦return await page.$eval(dom, (node) => node === document.activeElement);};const setValue = async (dom: string, value: string) => {//设置输入框内容const el = (await page.$(dom))!;await el.evaluate((node) => ((node as HTMLInputElement).value = ''));await el.type(value);};const setText = async (dom: string, value: string) => {//设置元素内容const el = (await page.$(dom))!;await el.evaluate((node) => ((node as HTMLElement).innerText = ''));await el.evaluate((node) => ((node as HTMLElement).innerText = value));};const enterValue = async (dom: string, value: string) => {//设置输入框内容后回车const el = (await page.$(dom))!;await el.evaluate((node) => ((node as HTMLInputElement).value = ''));await el.type(value);await el.press('Enter');};const clearValue = async (dom: string) => {//清空输入框内容await page.$eval(dom, (node) => ((node as HTMLInputElement).value = ''));};const timeout = async (time: number) => {//延时return page.evaluate((time) => {return new Promise((r) => {setTimeout(r, time);});}, time);};return {page: () => page,click,getCount,getText,getValue,getHtml,getChildrenCount,getClassList,domIsShow,isChecked,isFocused,setValue,setText,enterValue,clearValue,timeout,};
};export { getExampleUrl, setupPuppeteer, e2eTestTimeout };

尤大大的源码路径是这个:https://github.com/vuejs/vue/blob/main/test/e2e/e2eUtils.ts
以上代码主要做了这些事情:

  1. getExampleUrl函数用于获取测试的url地址,博主是测试组件库,因此就是每次测试单个组件文档的页面地址;
  2. setupPupeteer函数用于初始化测试环境(浏览器、打开页面)并且封装了一系列方法导出;

接下来看一下博主对于Form组件的测试:

import { getExampleUrl, setupPuppeteer, e2eTestTimeout } from '../e2eUtils';describe('form e2e test', () => {const { click, page, getCount, getText, getChildrenCount, setValue, getValue } = setupPuppeteer();const formTest = async () => {await page().goto(getExampleUrl('form'), {waitUntil: 'domcontentloaded',});//基本demo展示Dom测试expect(await getCount('#form-index1 .concis-form .concis-form-item')).toBe(4);expect(await getText('#form-index1 .concis-form .concis-form-item:nth-child(1) .concis-form-item-label',),).toBe('Username');expect(await getText('#form-index1 .concis-form .concis-form-item:nth-child(2) .concis-form-item-label',),).toBe('Post');expect(await getText('#form-index1 .concis-form .concis-form-item:nth-child(3) .concis-form-item-label',),).toBe('');expect(await getText('#form-index1 .concis-form .concis-form-item:nth-child(4) .concis-form-item-label',),).toBe('');//切换布局radioGroup测试expect(await getCount('#form-index2 .concis-radio-group')).toBe(1);//全局禁用测试expect(await getCount('#form-index5 .disabled')).toBe(1);//单行禁用测试expect(await getCount('#form-index7 .concis-form .concis-form-item:nth-child(2) .concis-form-item-disabled',),).toBe(1);//测试添加校验label icon显示expect(await getChildrenCount('#form-index8 .concis-form .concis-form-item:nth-child(1) .concis-form-item-label',),).toBe(1);//校验失败的测试await click('#form-index8 .concis-form .concis-form-item:nth-child(3) .concis-form-item-content',);expect(await getText('#form-index8 .concis-form .show-rule-label')).toBe('必须包含a');//提交后弹窗测试await click('#form-index9 .concis-form .concis-form-item:nth-child(3) .concis-form-item-content',);expect(await getChildrenCount('.all-container')).toBe(1);//测试重置await setValue('#form-index6 .concis-form .concis-form-item:nth-child(1) input', '123');expect(await getValue('#form-index6 .concis-form .concis-form-item:nth-child(1) input')).toBe('123',);await click('#form-index6 .concis-form .concis-form-item:nth-child(8) .concis-form-item-content .concis-button:nth-child(2)',);expect(await getValue('#form-index6 .concis-form .concis-form-item:nth-child(1) input')).toBe('',);};test('test e2e test', async () => await formTest(), e2eTestTimeout);
});

其实有了e2eUtils.ts后,编写测试用例方便了很多,只需要在调用封住好的api基础上加入jest的一些断言,就可以很好的测试组件的交互性。
这里Form的测试都有注释,博主不再依次阐述。

项目目录整理

由于之前只有单元测试,因此需要整理项目目录,整理后的目录如下:

对于package.json也是可以重新配置:

 "scripts": {"build": "rollup -c ./rollup.config.js","test:unit": "jest ./__tests__/unit",     //单元测试"test:e2e": "jest ./__tests__/e2e"            //e2e测试},

Concis组件库

博主自研Concis组件库已经有半年时间,组件库也是慢慢成型,目前已整合前端组件30+、组件unit/e2e测试、组件库文档、全局配置等等。

一路人也是就自己一个人,也是真的挺花费功夫的…博主也是需要更多的有兴趣小伙伴可以关注一下参与进来,一起贡献开源项目,建设一个基于Concis的技术社区,相互讨论、学习、帮助,这是博主从最初的兴趣想去实现一个小项目到目前为止一个更大的愿景。

更具体的可以参考 React组件库Concis,寻求社区中有兴趣的小伙伴加入…

Concis组件库线上链接:http://react-view-ui.com:92
github:https://github.com/fengxinhhh/Concis
npm:https://www.npmjs.com/package/concis

开源不易,欢迎学习和体验,喜欢请多多支持,有问题请留言,如果此文对你有帮助,博主需要你的支持,感谢。

基于Vue源码中e2e测试实践相关推荐

  1. 什么是php的ast结构,什么是AST?Vue源码中AST语法树的解析

    这篇文章给大家介绍的内容是关于什么是AST?Vue源码中AST语法树的解析,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助. 什么是AST AST是指抽象语法树(abstract syn ...

  2. Vue源码中的对象相等比较

    Vue源码中的对象比较函数写的极为经典漂亮,在此进行部分注释并添加一个函数类型比较判断,代码如下: function isObject(o) {return typeof o==='object'}f ...

  3. Vuex 4源码学习笔记 - 通过Vuex源码学习E2E测试(十一)

    在上一篇笔记中:Vuex 4源码学习笔记 - 做好changelog更新日志很重要(十) 我们学到了通过conventional-changelog来生成项目的Changelog更新日志,通过更新日志 ...

  4. vue源码中优秀代码片段(一)

    一.前言 笔者在读Vue源码时, 手记一些源码中优美的代码片段,一起来学习吧 二.代码片段 1. makeMap 检测某值是否在字符串(逗号分隔的字符串)中存在, 运用了柯里化函数和缓存函数 源码鉴赏 ...

  5. vue中rules校验是验证首字符_小白也能秒懂Vue源码中那些精细设计(选项处理)...

    我"崩"不住了,在彭凡同志锲而不舍的催促下这篇文章终于"蛋"生了. 说正经的这篇文章不好写,不好写的原因是我不太擅长写这些类比文,但它还是写出来了. 相信大部分 ...

  6. Vue源码中compiler部分逻辑梳理(内有彩蛋)

    [摘要] Vue compiler部分逻辑梳理 示例代码托管在:http://www.github.com/dashnowords/blogs 一. 简述 compiler模块Vue框架中用于模板编译 ...

  7. iris流程图_GitHub - LeoIris/vue: vue源码逐行注释分析+40多m的vue源码程序流程图思维导图 (diff部分待后续更新)...

    vue源码业余时间差不多看了一年,以前在网上找帖子,发现很多帖子很零散,都是一部分一部分说,断章的很多,所以自己下定决定一行行看,经过自己坚持与努力,现在基本看完了 .这个vue源码逐行分析,我基本每 ...

  8. 「从源码中学习」面试官都不知道的Vue题目答案

    前言 当回答面试官问及的Vue问题,我们除了照本宣科的回答外,其实还可以根据少量的源码来秀一把,来体现出你对Vue的深度了解. 本文会陆续更新,此次涉及以下问题: "new Vue()做了什 ...

  9. vue 拷贝 数组_vue源码中值得学习的方法

    最近在深入研究vue源码,把学习过程中,看到的一些好玩的的函数方法收集起来做分享,希望对大家对深入学习js有所帮助.如果大家都能一眼看懂这些函数,说明技术还是不错的哦. 1. 数据类型判断 Objec ...

最新文章

  1. 机器学习,满足人类情感:如何帮助电脑监控你的精神状态
  2. 创建topic验证kafka集群
  3. 十个同学想一起去周边城市两日游,分析方案的多/快/好等方面,并选择一种说明理由...
  4. Centos 6.2部署CSVN服务器
  5. Bzoj 4548: 小奇的糖果(双向链表+排序+树状数组)
  6. 12.多媒体和超链接标签及其应用实例
  7. 不同种类的ICP算法
  8. Jmeter BeanShell使用json.jar包处理Json数据
  9. Java学习——Java基本的程序设计结构笔记(一)
  10. select count(*) ,count() , select *
  11. matlab gui 保存文件,Matlab GUI的文件打開和保存uigetfile uigetdir
  12. css3实现3d正方体动画效果
  13. 《UNIX/LINUX系统管理I》课程学习总结
  14. mahout探索之旅---频繁模式挖掘算法与理解
  15. ArtyA7的Hello Word创建Microblaze嵌入式系统硬件工程的问题解决
  16. matlab中求矩阵A的特征向量,matlab层次分析法求特征值及特征向量.doc
  17. hue数据导出到hdfs_如何将智能灯泡迁移到新的Philips Hue Bridge
  18. 推进网络强国建设,筑牢网络安全屏障
  19. 泵机在线监测如此简单?后台数据观察一目了然
  20. 微信小程序项目——校园新闻网

热门文章

  1. day01 HTMLCSS
  2. java多用户商城系统架构之第一篇——总的介绍
  3. wormhole make 问题
  4. MCE | 新冠 德尔塔病毒
  5. 2019年Android开发的未来发展方向该如何走?
  6. Iphone手机企业邮箱设置 九步轻松搞定
  7. 海康摄像头实现点位缩放功能(切换焦距)
  8. form表单的enctype
  9. 窗体内公用的数据表,在使用视图和行过滤时的有趣现象
  10. Vue常用的内置指令的底层细节分析