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


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 相关文章推荐
js切换div css注意的细节
Dec 10 Javascript
Javascript事件实例详解
Nov 06 Javascript
jQuery实现模拟marquee标签效果
Jul 14 Javascript
js实现非常棒的弹出div
Oct 06 Javascript
AngularJs上传前预览图片的实例代码
Jan 20 Javascript
5分钟学会Vue动画效果(小结)
Jul 21 Javascript
如何从头实现一个node.js的koa框架
Jun 17 Javascript
JavaScript 斐波那契数列 倒序输出 输出100以内的质数代码实例
Sep 11 Javascript
Javascript实现鼠标移入方向感知
Jun 24 Javascript
微信小程序自定义底部弹出框动画
Nov 18 Javascript
js实现碰撞检测
Jan 29 Javascript
JavaScript如何利用Promise控制并发请求个数
May 14 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
如何过滤高亮显示非法字符
2006/10/09 PHP
PHP5+UTF8多文件上传类
2008/10/17 PHP
Yii2创建控制器(createController)方法详解
2016/07/23 PHP
Laravel+jQuery实现AJAX分页效果
2016/09/14 PHP
PHP构造函数与析构函数用法示例
2016/09/28 PHP
php源码之将图片转化为data/base64数据流实例详解
2016/11/27 PHP
Yii2下点击验证码的切换实例代码
2017/03/14 PHP
js类后台管理菜单类-MenuSwitch
2007/09/12 Javascript
js 绑定键盘鼠标事件示例代码
2014/02/12 Javascript
javascript 回调函数详解
2014/11/11 Javascript
浅谈JavaScript的事件
2015/02/27 Javascript
浅谈js的url解析函数封装
2016/06/28 Javascript
JS防止网页被嵌入iframe框架的方法分析
2016/09/13 Javascript
使用contextMenu插件实现Bootstrap table弹出右键菜单
2017/02/20 Javascript
利用百度地图API获取当前位置信息的实例
2017/11/06 Javascript
小程序开发基础之view视图容器
2018/08/21 Javascript
Python读写Redis数据库操作示例
2014/03/18 Python
Win7下搭建python开发环境图文教程(安装Python、pip、解释器)
2016/05/17 Python
python中datetime模块中strftime/strptime函数的使用
2018/07/03 Python
使用tensorflow实现线性svm
2018/09/07 Python
对Python通过pypyodbc访问Access数据库的方法详解
2018/10/27 Python
python SVM 线性分类模型的实现
2019/07/19 Python
Django中从mysql数据库中获取数据传到echarts方式
2020/04/07 Python
理解Django 中Call Stack机制的小Demo
2020/09/01 Python
python3从网络摄像机解析mjpeg http流的示例
2020/11/13 Python
python安装及变量名介绍详解
2020/12/12 Python
英国快时尚女装购物网站:PrettyLittleThing
2018/08/15 全球购物
莫斯科高科技在线商店:KremlinStore
2019/03/13 全球购物
PHP如何对用户密码进行加密
2014/07/31 面试题
建筑工程实习自我鉴定
2013/09/19 职场文书
会计职业生涯规划书
2014/01/13 职场文书
美国探亲签证邀请信
2014/02/05 职场文书
婚礼家长致辞
2015/07/27 职场文书
小学数学教师研修感悟
2015/11/18 职场文书
2016关于读书活动的心得体会
2016/01/14 职场文书
python 单机五子棋对战游戏
2022/04/28 Python