小程序自动化测试
发表时间:2020-10-26
发布人:葵宇科技
浏览次数:51
背景
近期团队打算做一个小程序自动化测试的工具,期望能够做的业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布时候会影响小程序的基础功能。
上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原。
自动化 SDK
如何将操作路径还原这个问题,当然首选官方提供的 SDK: miniprogram-automator 。
小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。通过该 SDK,你可以做到以下事情:
-
控制小程序跳转到指定页面
获取小程序页面数据
获取小程序页面元素状态
触发小程序元素绑定事件
往 AppService 注入代码片段
调用 wx 对象上任意接口
…
上面的描述都来自官方文档,建议阅读后面内容之前可以先看看 官方文档 ,当然如果之前用过 puppeteer ,基本是无缝衔接。下面简单介绍下 SDK 的使用方式。
如果对软件测试、接口、自动化、性能测试、测试开发、面试经验交流。感兴趣可以1079636098,群内会有不定期的发放免费的资料链接,这些资料都是从各个技术网站搜集、整理出来的,如果你有好的学习资料可以私聊发我,我会注明出处之后分享给大家。
// 引入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 中捕获所有的事件,好在小程序所以的页面和组件都必须通过 Page 、 Component 方法来包装,所以我们可以改写这两个方法,拦截传入的方法,并判断第一个参数是否为 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 的 rel="stylesheet">