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


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类
Sep 08 Javascript
【消息提示组件】,兼容IE6/7&amp;&amp;FF2
Sep 04 Javascript
JavaScript中使用replace结合正则实现replaceAll的效果
Jun 04 Javascript
JS 在指定数组中随机取出N个不重复的数据
Jun 10 Javascript
javascript实现随机读取数组的方法
Aug 03 Javascript
JavaScript中函数声明与函数表达式的区别详解
Aug 18 Javascript
Bootstrap源码解读排版(1)
Dec 23 Javascript
详解axios在node.js中的post使用
Apr 27 Javascript
js+SVG实现动态时钟效果
Jul 14 Javascript
JavaScript数据结构与算法之基本排序算法定义与效率比较【冒泡、选择、插入排序】
Feb 21 Javascript
JS实现获取数组中最大值或最小值功能示例
Mar 02 Javascript
详解vue-router的Import异步加载模块问题的解决方案
May 13 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
php笔记之:php函数range() round()和list()的使用说明
2013/04/26 PHP
功能强大的PHP图片处理类(水印、透明度、旋转)
2015/10/21 PHP
使用phpstorm和xdebug实现远程调试的方法
2015/12/29 PHP
Yii2框架制作RESTful风格的API快速入门教程
2016/11/08 PHP
php高清晰度无损图片压缩功能的实现代码
2018/12/09 PHP
利用jQuery的$.event.fix函数统一浏览器event事件处理
2009/12/21 Javascript
jquery下组织javascript代码(js函数化)
2010/08/25 Javascript
客户端限制只能上传jpg格式图片的js代码
2010/12/09 Javascript
基于jQuery的计算文本框字数的代码
2012/06/06 Javascript
jquery 操作css样式、位置、尺寸方法汇总
2014/11/28 Javascript
Javascript刷新窗口方法小结
2015/10/21 Javascript
JavaScript原生xmlHttp与jquery的ajax方法json数据格式实例
2015/12/04 Javascript
js防阻塞加载的实现方法
2016/09/09 Javascript
javascript字符串对象常用api函数小结(连接,替换,分割,转换等)
2016/09/20 Javascript
常用的javascript设计模式
2017/01/11 Javascript
bootstrap weebox 支持ajax的模态弹出框
2017/02/23 Javascript
EasyUI为Numberbox添加blur事件的方法
2017/03/05 Javascript
Node.js v8.0.0正式发布!看看带来了哪些主要新特性
2017/06/02 Javascript
vue 自定义指令自动获取文本框焦点的方法
2018/08/25 Javascript
理解JavaScript中的Proxy 与 Reflection API
2020/09/21 Javascript
Javascript中的奇葩知识,你知道吗?
2021/01/25 Javascript
Python3实现连接SQLite数据库的方法
2014/08/23 Python
Python编程实现二分法和牛顿迭代法求平方根代码
2017/12/04 Python
快速了解Python开发中的cookie及简单代码示例
2018/01/17 Python
Python中的单继承与多继承实例分析
2018/05/10 Python
对python多线程SSH登录并发脚本详解
2019/02/14 Python
python3+PyQt5 实现Rich文本的行编辑方法
2019/06/17 Python
简单了解python变量的作用域
2019/07/30 Python
涉外文秘个人求职的自我评价
2013/10/07 职场文书
学生思想表现的评语
2014/01/30 职场文书
安全教育感言
2014/03/04 职场文书
土地租赁意向书
2014/07/30 职场文书
《清澈的湖水》教学反思
2016/02/17 职场文书
深入理解CSS 中 transform matrix矩阵变换问题
2021/08/30 HTML / CSS
Win2008系统搭建DHCP服务器
2022/06/25 Servers
MySQL实现用逗号进行拼接、以逗号进行分割
2022/12/24 MySQL