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

背景

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

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

自动化 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。

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

(0)

相关推荐

  • 小程序测试后台服务的方法(ngrok)

    什么是ngrok? 官方解释是这样说的: ngrok 是一个反向代理,通过在公共的端点和本地运行的 Web 服务器之间建立一个安全的通道. 这段话是什么意思? 运行ngrok服务以后,本地运行的Web服务会被ngrok代理.当你访问它给你提供的域名时候,它会经过公共的端点解析到本地,这样本地的服务就可以通过外网访问了.(看不懂也没关系,会用就行了) 如何使用? 这里告诉大家一个国内搭建的 Ngrok国内免费服务器-小米球,使用起来更方便灵活. 服务成功启动以后长这样,表示把这个域名映射到本地,支

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

    背景 近期团队打算做一个小程序自动化测试的工具,期望能够做的业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布时候会影响小程序的基础功能. 上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原. 自动化 SDK 如何将操作路径还原这个问题,当然首选官方提供的 SDK: miniprogram-automator . 小程序自动化 SDK为开发者提供了一套通过外

  • 生成无限制的微信小程序码的示例代码

    概述 现在除了用二维码之外,微信还可以为我们生成小程序码,大概长这个样子. 如果要生成无限制的小程序码,需要几个步骤 1.小程序某个页面的地址,也即是指定page参数的值: 2.获取access_token; 3.指定scene参数的值: 4.调用getwxacodeunlimit接口,并将返回结果存储到一个图片里. page参数 page参数的值都是以pages开头的,后面加上能到导航到某个小程序页面的路径,例如: pages/xxxxxx 获取access_token 这个比较简单,只需要根

  • Django实现微信小程序支付的示例代码

    1.下载相关的库 微信官方已经提供了方便开发者的SDK,可是使用pip方式下载: pip install wechatpy 2. 在项目的settings.py文件添加相关配置 具体的参数需要自己到小程序微信公众平台和微信商户平台获取. WECHAT = { 'APPID': 'appid', # 小程序ID 'APPSECRET': 'appsecret', # 小程序SECRET 'MCH_ID': 'mch_id', # 商户号 'TOTAL_FEE': '1', # 总金额, 单位为"分

  • C语言小程序 数组操作示例代码

    复制代码 代码如下: #include <stdio.h>#include <stdlib.h>#include <time.h>int size = 0;int flag = 0;void output(int *arry){ int i = 0; for(i=0; i<size; i++) {  printf("arry[%d]=%d\t",i,arry[i]);  if((i+1)%5 == 0)   printf("\n&qu

  • 微信小程序前端promise封装代码实例

    这篇文章主要介绍了微信小程序前端promise封装代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 代码如下 config.js const config = { base_url_api : "https://douban.uieee.com/v2/movie/", } export {config} http.js import { config } from "../config"; class HTT

  • 微信小程序 简易计算器实现代码实例

    这篇文章主要介绍了微信小程序 简易计算器实现代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 只能进行简单的运算 效果图如下: cal.wxml <view class="screen">{{result}}</view> <view class="content"> <view class="buttonGroup"> <button

  • 微信小程序 搜索框组件代码实例

    这篇文章主要介绍了微信小程序 搜索框组件代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 代码如下 search.wxml <view class="header"> <view class="search"> <icon type="search" size="18" color=""> </icon>

  • 微信小程序进入广告实现代码实例

    这篇文章主要介绍了微信小程序进入广告实现代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 代码如下 <view class="container"> <image src="../../imgs/swiper1.jpg"></image> <text bindtap="cliadv">跳过广告 {{miao}}</text> &l

  • 微信小程序实现禁止分享代码实例

    这篇文章主要介绍了微信小程序实现禁止分享代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 微信禁止分享 添加以下代码到网页中即可 <script> document.addEventListener('WeixinJSBridgeReady', function onBridgeReady() { // 通过下面这个API隐藏右上角按钮 WeixinJSBridge.call('hideOptionMenu'); // 通过下面这个AP

  • 微信小程序关键字变色实现代码实例

    这篇文章主要介绍了微信小程序关键字变色实现代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1.小程序中不支持渲染indexOf使用,我们可以通过新建wps来实现 function fn(arr, arg) { var result = { indexOf: false, toString: '' } result.indexOf = arr.indexOf(arg) > -1; result.toString = arr.join(&quo

随机推荐