小程序自动化测试的示例代码


Posted in Javascript onAugust 11, 2020

背景

近期团队打算做一个小程序自动化测试的工具,期望能够做的业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布时候会影响小程序的基础功能。

小程序自动化测试的示例代码

上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原。

自动化 SDK

如何将操作路径还原这个问题,当然首选官方提供的 SDK: miniprogram-automator

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。通过该 SDK,你可以做到以下事情:

  • 控制小程序跳转到指定页面
  • 获取小程序页面数据
  • 获取小程序页面元素状态
  • 触发小程序元素绑定事件
  • 往 AppService 注入代码片段
  • 调用 wx 对象上任意接口
  • ...

上面的描述都来自官方文档,建议阅读后面内容之前可以先看看官方文档 ,当然如果之前用过 puppeteer ,基本是无缝衔接。下面简单介绍下 SDK 的使用方式。

// 引入sdk
const automator = require('miniprogram-automator')

// 启动微信开发者工具
automator.launch({
 // 微信开发者工具安装路径下的 cli 工具
 // Windows下为安装路径下的 cli.bat
 // MacOS下为安装路径下的 cli
 cliPath: 'path/to/cli',
 // 项目地址,即要运行的小程序的路径
 projectPath: 'path/to/project',
}).then(async miniProgram => { // miniProgram 为 IDE 启动后的实例
 // 启动小程序里的 index 页面
 const page = await miniProgram.reLaunch('/page/index/index')
 // 等待 500 ms
 await page.waitFor(500)
 // 获取页面元素
 const element = await page.$('.main-btn')
 // 点击元素
 await element.tap()
 // 关闭 IDE
 await miniProgram.close()
})

有个地方需要提醒一下:使用 SDK 之前需要开启开发者工具的服务端口,要不然会启动失败。

小程序自动化测试的示例代码

捕获用户行为

有了还原操作路径的办法,接下来就要解决记录操作路径的难题了。

在小程序中,并不能像 web 中通过事件冒泡的方式在 window 中捕获所有的事件,好在小程序所以的页面和组件都必须通过 PageComponent 方法来包装,所以我们可以改写这两个方法,拦截传入的方法,并判断第一个参数是否为 event 对象,以此来捕获所有的事件。

// 暂存原生方法
const originPage = Page
const originComponent = Component

// 改写 Page
Page = (params) => {
 const names = Object.keys(params)
 for (const name of names) {
 // 进行方法拦截
 if (typeof obj[name] === 'function') {
  params[name] = hookMethod(name, params[name], false)
 }
 }
 originPage(params)
}
// 改写 Component
Component = (params) => {
 if (params.methods) {
  const { methods } = params
  const names = Object.keys(methods)
  for (const name of names) {
  // 进行方法拦截
  if (typeof methods[name] === 'function') {
   methods[name] = hookMethod(name, methods[name], true)
  }
  }
 }
 originComponent(params)
}

const hookMethod = (name, method, isComponent) => {
 return function(...args) {
 const [evt] = args // 取出第一个参数
 // 判断是否为 event 对象
 if (evt && evt.target && evt.type) {
  // 记录用户行为
 }
 return method.apply(this, args)
 }
}

这里的代码只是代理了所有的事件方法,并不能用来还原用户的行为,要还原用户行为还必须知道该事件类型是否是需要的,比如点击、长按、输入。

const evtTypes = [
 'tap', // 点击
 'input', // 输入
 'confirm', // 回车
 'longpress' // 长按
]
const hookMethod = (name, method) => {
 return function(...args) {
 const [evt] = args // 取出第一个参数
 // 判断是否为 event 对象
 if (
  evt && evt.target && evt.type &&
  evtTypes.includes(evt.type) // 判断事件类型
 ) {
  // 记录用户行为
 }
 return method.apply(this, args)
 }
}

确定事件类型之后,还需要明确点击的元素到底是哪个,但是小程序里面比较坑的地方就是,event 对象的 target 属性中,并没有元素的类名,但是可以获取元素的 dataset。

小程序自动化测试的示例代码

为了准确的获取元素,我们需要在构建中增加一个步骤,修改 wxml 文件,将所以元素的 class 属性复制一份到 data-className

<!-- 构建前 -->
<view class="close-btn"></view>
<view class="{{mainClassName}}"></view>
<!-- 构建后 -->
<view class="close-btn" data-className="close-btn"></view>
<view class="{{mainClassName}}" data-className="{{mainClassName}}"></view>

但是获取到 class 之后,又会有另一个坑,小程序的自动化测试工具并不能直接获取页面里自定义组件中的元素,必须先获取自定义组件。

<!-- Page -->
<toast text="loading" show="{{showToast}}" />
<!-- Component -->
<view class="toast" wx:if="{{show}}">
 <text class="toast-text">{{text}}</text>
 <view class="toast-close" />
</view>
// 如果直接查找 .toast-close 会得到 null
const element = await page.$('.toast-close')
element.tap() // Error!

// 必须先通过自定义组件的 tagName 找到自定义组件
// 再从自定义组件中通过 className 查找对应元素
const element = await page.$('toast .toast-close')
element.tap()

所以我们在构建操作的时候,还需要为元素插入 tagName。

<!-- 构建前 -->
<view class="close-btn" />
<toast text="loading" show="{{showToast}}" />
<!-- 构建后 -->
<view class="close-btn" data-className="close-btn" data-tagName="view" />
<toast text="loading" show="{{showToast}}" data-tagName="toast" />

现在我们可以继续愉快的记录用户行为了。

// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
 actions.push({
 time: Date.now(),
 type,
 query,
 value
 })
}

// 代理事件方法
const hookMethod = (name, method, isComponent) => {
 return function(...args) {
 const [evt] = args // 取出第一个参数
 // 判断是否为 event 对象
 if (
  evt && evt.target && evt.type &&
  evtTypes.includes(evt.type) // 判断事件类型
 ) {
  const { type, target, detail } = evt
  const { id, dataset = {} } = target
  const { className = '' } = dataset
  const { value = '' } = detail // input事件触发时,输入框的值
  // 记录用户行为
  let query = ''
  if (isComponent) {
  // 如果是组件内的方法,需要获取当前组件的 tagName
  query = `${this.dataset.tagName} `
  }
  if (id) {
  // id 存在,则直接通过 id 查找元素
  query += id
  } else {
  // id 不存在,才通过 className 查找元素
  query += className
  }
  addAction(type, query, value)
 }
 return method.apply(this, args)
 }
}

到这里已经记录了用户所有的点击、输入、回车相关的操作,但是还有一个滚动屏幕的操作还没记录。这里可以直接监听 Page 的 onPageScroll。

// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
 if (type === 'scroll' || type === 'input') {
 // 如果上一次行为也是滚动或输入,则重置 value 即可
 const last = this.actions[this.actions.length - 1]
 if (last && last.type === type) {
  last.value = value
  last.time = Date.now()
  return
 }
 }
 actions.push({
 time: Date.now(),
 type,
 query,
 value
 })
}

Page = (params) => {
 const names = Object.keys(params)
 for (const name of names) {
 // 进行方法拦截
 if (typeof obj[name] === 'function') {
  params[name] = hookMethod(name, params[name], false)
 }
 }
 const { onPageScroll } = params
 // 拦截滚动事件
 params.onPageScroll = function (...args) {
 const [evt] = args
 const { scrollTop } = evt
 addAction('scroll', '', scrollTop)
 onPageScroll.apply(this, args)
 }
 originPage(params)
}

这里有个优化点,就是滚动操作记录的时候,可以判断一下上次操作是否也为滚动操作,如果是同一个操作,则只需要修改一下滚动距离即可,以为两次滚动可以一步到位。同理,输入事件也是,输入的值也可以一步到位。

还原用户行为

用户操作完毕后,可以在控制台输出用户行为的 json 文本,把 json 文本复制出来后,就可以通过自动化工具运行了。

// 引入sdk
const automator = require('miniprogram-automator')

// 用户操作行为
const actions = [
 { type: 'tap', query: 'goods .title', value: '', time: 1596965650000 },
 { type: 'scroll', query: '', value: 560, time: 1596965710680 },
 { type: 'tap', query: 'gotoTop', value: '', time: 1596965770000 }
]

// 启动微信开发者工具
automator.launch({
 projectPath: 'path/to/project',
}).then(async miniProgram => {
 let page = await miniProgram.reLaunch('/page/index/index')
 
 let prevTime
 for (const action of actions) {
 const { type, query, value, time } = action
 if (prevTime) {
  // 计算两次操作之间的等待时间
  await page.waitFor(time - prevTime)
 }
 // 重置上次操作时间
 prevTime = time
 
 // 获取当前页面实例
 page = await miniProgram.currentPage()
 switch (type) {
  case 'tap':
   const element = await page.$(query)
  await element.tap()
  break;
  case 'input':
   const element = await page.$(query)
  await element.input(value)
  break;
  case 'confirm':
   const element = await page.$(query)
    await element.trigger('confirm', { value });
  break;
  case 'scroll':
  await miniProgram.pageScrollTo(value)
  break;
 }
 // 每次操作结束后,等待 5s,防止页面跳转过程中,后面的操作找不到页面
 await page.waitFor(5000)
 }

 // 关闭 IDE
 await miniProgram.close()
})

这里只是简单的还原了用户的操作行为,实际运行过程中,还会涉及到网络请求和 localstorage 的 mock,这里不再展开讲述。同时,我们还可以接入 jest 工具,更加方便用例的编写。

总结

看似很难的需求,只要用心去发掘,总能找到对应的解决办法。另外微信小程序的自动化工具真的有很多坑,遇到问题可以先到小程序社区去找找,大部分坑都有前人踩过,还有一些一时无法解决的问题只能想其他办法来规避。最后祝愿天下无 bug。

到此这篇关于小程序自动化测试的示例代码的文章就介绍到这了,更多相关小程序自动化测试内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
javascript中String类的subString()方法和slice()方法
May 24 Javascript
两种常用的javascript数组去重方法思路及代码
Mar 26 Javascript
用JavaScript实现使用鼠标画线的示例代码
Aug 19 Javascript
JavaScript检测弹出窗口是否已经关闭的方法
Mar 24 Javascript
js ajaxfileupload.js上传报错的解决方法
May 05 Javascript
jQuery 获取多选框的值及多选框中文的函数
May 16 Javascript
使用 jQuery 实现表单验证功能
Jul 05 jQuery
vue项目实现表单登录页保存账号和密码到cookie功能
Aug 31 Javascript
ng-repeat指令在迭代对象时的去重方法
Oct 02 Javascript
微信小程序图片左右摆动效果详解
Jul 13 Javascript
Vue开发环境中修改端口号的实现方法
Aug 15 Javascript
Vue.js如何使用Socket.IO的示例代码
Sep 05 Javascript
vue自定义组件(通过Vue.use()来使用)即install的用法说明
Aug 11 #Javascript
Vue 实现创建全局组件,并且使用Vue.use() 载入方式
Aug 11 #Javascript
Vue自定义全局弹窗组件操作
Aug 11 #Javascript
基于Vue全局组件与局部组件的区别说明
Aug 11 #Javascript
VUE使用 wx-open-launch-app 组件开发微信打开APP功能
Aug 11 #Javascript
vue实现图片按比例缩放问题操作
Aug 11 #Javascript
JavaScript中while循环的基础使用教程
Aug 11 #Javascript
You might like
Thinkphp中Create方法深入探究
2014/06/16 PHP
PHP网页游戏学习之Xnova(ogame)源码解读(十六)
2014/06/30 PHP
php实现文章评论系统
2019/02/18 PHP
编写Js代码要注意的几条规则
2010/09/10 Javascript
瀑布流布局代码一例
2014/04/11 Javascript
jQuery分组选择器用法实例
2014/12/23 Javascript
jQuery插件pagewalkthrough实现引导页效果
2015/07/05 Javascript
JS模拟Dialog弹出浮动框效果代码
2015/10/16 Javascript
js检查是否关闭浏览器的方法
2016/08/02 Javascript
js HTML5 Canvas绘制转盘抽奖
2020/09/13 Javascript
nodejs 搭建简易服务器的图文教程(推荐)
2017/07/18 NodeJs
微信小程序录音与播放录音功能
2017/12/25 Javascript
angular 组件通信的几种实现方式
2018/07/13 Javascript
Smartour 让网页导览变得更简单(推荐)
2019/07/19 Javascript
教你搭建按需加载的Vue组件库(小结)
2019/07/29 Javascript
React实现轮播效果
2020/08/25 Javascript
js调用网络摄像头的方法
2020/12/05 Javascript
js实现验证码干扰(静态)
2021/02/22 Javascript
[14:57]DOTA2 HEROS教学视频教你分分钟做大人-幽鬼
2014/06/13 DOTA
[53:13]DOTA2-DPC中国联赛 正赛 DLG vs PHOENIX BO3 第三场 1月18日
2021/03/11 DOTA
Python 使用 attrs 和 cattrs 实现面向对象编程的实践
2019/06/12 Python
Gauss-Seidel迭代算法的Python实现详解
2019/06/29 Python
python OpenCV GrabCut使用实例解析
2019/11/11 Python
详解Python+Selenium+ChromeDriver的配置和问题解决
2021/01/19 Python
教你如何一步一步用Canvas写一个贪吃蛇
2018/10/22 HTML / CSS
HTML5验证以及日期显示的实现详解
2013/07/05 HTML / CSS
波兰最大的宠物用品网上商店:FERA.PL
2019/08/11 全球购物
Gibson London官网:以地道的英国男装而著称
2019/12/06 全球购物
九州传奇上机题
2014/07/10 面试题
文职个人求职信范文
2013/09/23 职场文书
英文感谢信格式
2015/01/21 职场文书
英文版辞职信
2015/02/28 职场文书
Css预编语言及区别详解
2021/04/25 HTML / CSS
MySQL 如何限制一张表的记录数
2021/09/14 MySQL
MySQL表锁、行锁、排它锁及共享锁的使用详解
2022/04/02 MySQL
mysql中如何用命令创建联合唯一索引
2022/04/20 MySQL