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


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 相关文章推荐
IE下写xml文件的两种方式(fso/saveAs)
Aug 05 Javascript
json格式的时间显示为正常年月日的方法
Sep 08 Javascript
jQuery插件 selectToSelect使用方法
Oct 02 Javascript
一行命令搞定node.js 版本升级
Jul 20 Javascript
JavaScript字符串对象toUpperCase方法入门实例(用于把字母转换为大写)
Oct 17 Javascript
JS建造者模式基本用法实例分析
Jun 30 Javascript
详解使用Vue.Js结合Jquery Ajax加载数据的两种方式
Jan 10 Javascript
AngularJS的ng-click传参的方法
Jun 19 Javascript
Django使用多数据库的方法
Sep 06 Javascript
详解vue几种主动刷新的方法总结
Feb 19 Javascript
详解基于Wepy开发小程序插件(推荐)
Aug 01 Javascript
vue中使用element组件时事件想要传递其他参数的问题
Sep 18 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 AJAX实例根据邮编自动完成地址信息
2008/11/23 PHP
php中static静态变量的使用方法详解
2010/06/04 PHP
php提交表单发送邮件的方法
2015/03/20 PHP
php中smarty区域循环的方法
2015/06/11 PHP
ThinkPHP框架中使用Memcached缓存数据的方法
2018/03/31 PHP
ThinkPHP5.0框架控制器继承基类和自定义类示例
2018/05/25 PHP
为JavaScript类型增加方法的实现代码(增加功能)
2011/12/29 Javascript
推荐8款jQuery轻量级树形Tree插件
2014/11/12 Javascript
js控制div弹出层实现方法
2015/05/11 Javascript
javascript Promise简单学习使用方法小结
2016/05/17 Javascript
使用Script元素发送JSONP请求的方法
2016/06/12 Javascript
jQuery-mobile事件监听与用法详解
2016/11/23 Javascript
从零学习node.js之express入门(六)
2017/02/25 Javascript
js实现仿购物车加减效果
2017/03/01 Javascript
响应式框架Bootstrap栅格系统的实例
2017/12/19 Javascript
Node.js Express安装与使用教程
2018/05/11 Javascript
JS计算斐波拉切代码实例
2019/09/12 Javascript
SSM+layUI 根据登录信息显示不同的页面方法
2019/09/20 Javascript
nodejs制作小爬虫功能示例
2020/02/24 NodeJs
python中字符串类型json操作的注意事项
2017/05/02 Python
Python实现替换文件中指定内容的方法
2018/03/19 Python
python如何爬取网站数据并进行数据可视化
2019/07/08 Python
用Pytorch训练CNN(数据集MNIST,使用GPU的方法)
2019/08/19 Python
Python调用scp向服务器上传文件示例
2019/12/22 Python
Python基础之字符串操作常用函数集合
2020/02/09 Python
树莓派升级python的具体步骤
2020/07/05 Python
Python matplotlib读取excel数据并用for循环画多个子图subplot操作
2020/07/14 Python
CSS3中使用RGBA设置透明度的示例
2015/08/04 HTML / CSS
CSS3 实现发光边框特效
2020/11/11 HTML / CSS
英国网上电器商店:Electricshop
2020/03/15 全球购物
一年级班主任寄语
2014/01/19 职场文书
大学活动总结格式
2014/04/29 职场文书
创建青年文明号材料
2014/05/09 职场文书
2015年电气技术员工作总结
2015/07/24 职场文书
HR必备:销售经理聘用合同范本
2019/08/21 职场文书
Android开发之底部导航栏的快速实现
2022/04/28 Java/Android