知識
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價值,我們在追求其視覺表現(xiàn)的同時,更側(cè)重于功能的便捷,營銷的便利,運(yùn)營的高效,讓網(wǎng)站成為營銷工具,讓軟件能切實提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序為后期升級提供便捷的支持!
您當(dāng)前位置>首頁 » 新聞資訊 » 小程序相關(guān) >
小程序自動化測試
發(fā)表時間:2021-1-11
發(fā)布人:葵宇科技
瀏覽次數(shù):37
背景
近期團(tuán)隊打算做一個小程序自動化測試的工具,期望能夠做的業(yè)務(wù)人員操作一遍小程序后,自動還原之前的操作路徑,并且捕獲操作過程中發(fā)生的異常,以此來判斷這次發(fā)布時候會影響小程序的基礎(chǔ)功能。
上述描述看似簡單,但是中間還是有些難點的,第一個難點就是如何在業(yè)務(wù)人員操作小程序的時候記錄操作路徑,第二個難點就是如何將記錄的操作路徑進(jìn)行還原。
自動化 SDK
如何將操作路徑還原這個問題,當(dāng)然首選官方提供的 SDK: miniprogram-automator
。
小程序自動化 SDK 為開發(fā)者提供了一套通過外部腳本操控小程序的方案,從而實現(xiàn)小程序自動化測試的目的。通過該 SDK,你可以做到以下事情:
- 控制小程序跳轉(zhuǎn)到指定頁面
- 獲取小程序頁面數(shù)據(jù)
- 獲取小程序頁面元素狀態(tài)
- 觸發(fā)小程序元素綁定事件
- 往 AppService 注入代碼片段
- 調(diào)用 wx 對象上任意接口
- ...
上面的描述都來自官方文檔,建議閱讀后面內(nèi)容之前可以先看看 官方文檔 ,當(dāng)然如果之前用過 puppeteer ,基本是無縫銜接。下面簡單介紹下 SDK 的使用方式。
// 引入sdk
const automator = require('miniprogram-automator')
// 啟動微信開發(fā)者工具
automator.launch({
// 微信開發(fā)者工具安裝路徑下的 cli 工具
// Windows下為安裝路徑下的 cli.bat
// MacOS下為安裝路徑下的 cli
cliPath: 'path/to/cli',
// 項目地址,即要運(yùn)行的小程序的路徑
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()
// 關(guān)閉 IDE
await miniProgram.close()
})
復(fù)制代碼
有個地方需要提醒一下:使用 SDK 之前需要開啟開發(fā)者工具的服務(wù)端口,要不然會啟動失敗。

捕獲用戶行為
有了還原操作路徑的辦法,接下來就要解決記錄操作路徑的難題了。
在小程序中,并不能像 web 中通過事件冒泡的方式在 window 中捕獲所有的事件,好在小程序所以的頁面和組件都必須通過 Page
、 Component
方法來包裝,所以我們可以改寫這兩個方法,攔截傳入的方法,并判斷第一個參數(shù)是否為 event
對象,以此來捕獲所有的事件。
// 暫存原生方法
const originPage = Page
const originComponent = Component
// 改寫 Page
Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 進(jìn)行方法攔截
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) {
// 進(jìn)行方法攔截
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 // 取出第一個參數(shù)
// 判斷是否為 event 對象
if (evt && evt.target && evt.type) {
// 記錄用戶行為
}
return method.apply(this, args)
}
}
復(fù)制代碼
這里的代碼只是代理了所有的事件方法,并不能用來還原用戶的行為,要還原用戶行為還必須知道該事件類型是否是需要的,比如點擊、長按、輸入。
const evtTypes = [
'tap', // 點擊
'input', // 輸入
'confirm', // 回車
'longpress' // 長按
]
const hookMethod = (name, method) => {
return function(...args) {
const [evt] = args // 取出第一個參數(shù)
// 判斷是否為 event 對象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判斷事件類型
) {
// 記錄用戶行為
}
return method.apply(this, args)
}
}
復(fù)制代碼
確定事件類型之后,還需要明確點擊的元素到底是哪個,但是小程序里面比較坑的地方就是,event 對象的 target 屬性中,并沒有元素的類名,但是可以獲取元素的 dataset。

為了準(zhǔn)確的獲取元素,我們需要在構(gòu)建中增加一個步驟,修改 wxml 文件,將所以元素的 class
屬性復(fù)制一份到 data-className
。
<!-- 構(gòu)建前 -->
<view class="close-btn"></view>
<view class="{{mainClassName}}"></view>
<!-- 構(gòu)建后 -->
<view class="close-btn" data-className="close-btn"></view>
<view class="{{mainClassName}}" data-className="{{mainClassName}}"></view>
復(fù)制代碼
但是獲取到 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>
復(fù)制代碼
// 如果直接查找 .toast-close 會得到 null
const element = await page.$('.toast-close')
element.tap() // Error!
// 必須先通過自定義組件的 tagName 找到自定義組件
// 再從自定義組件中通過 className 查找對應(yīng)元素
const element = await page.$('toast .toast-close')
element.tap()
復(fù)制代碼
所以我們在構(gòu)建操作的時候,還需要為元素插入 tagName。
<!-- 構(gòu)建前 -->
<view class="close-btn" />
<toast text="loading" show="{{showToast}}" />
<!-- 構(gòu)建后 -->
<view class="close-btn" data-className="close-btn" data-tagName="view" />
<toast text="loading" show="{{showToast}}" data-tagName="toast" />
復(fù)制代碼
現(xiàn)在我們可以繼續(xù)愉快的記錄用戶行為了。
// 記錄用戶行為的數(shù)組
const actions = [];
// 添加用戶行為
const addAction = (type, query, value = http://www.wxapp-union.com/'') => {
actions.push({
time: Date.now(),
type,
query,
value
})
}
// 代理事件方法
const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一個參數(shù)
// 判斷是否為 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 = http://www.wxapp-union.com/'' } = detail // input事件觸發(fā)時,輸入框的值
// 記錄用戶行為
let query = ''
if (isComponent) {
// 如果是組件內(nèi)的方法,需要獲取當(dāng)前組件的 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)
}
}
復(fù)制代碼
到這里已經(jīng)記錄了用戶所有的點擊、輸入、回車相關(guān)的操作,但是還有一個滾動屏幕的操作還沒記錄。這里可以直接監(jiān)聽 Page 的 onPageScroll。
// 記錄用戶行為的數(shù)組
const actions = [];
// 添加用戶行為
const addAction = (type, query, value = http://www.wxapp-union.com/'') => {
if (type === 'scroll' || type === 'input') {
// 如果上一次行為也是滾動或輸入,則重置 value 即可
const last = this.actions[this.actions.length - 1]
if (last && last.type === type) {
last.value = http://www.wxapp-union.com/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) {
// 進(jìn)行方法攔截
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)
}
復(fù)制代碼
這里有個優(yōu)化點,就是滾動操作記錄的時候,可以判斷一下上次操作是否也為滾動操作,如果是同一個操作,則只需要修改一下滾動距離即可,以為兩次滾動可以一步到位。同理,輸入事件也是,輸入的值也可以一步到位。
還原用戶行為
用戶操作完畢后,可以在控制臺輸出用戶行為的 json 文本,把 json 文本復(fù)制出來后,就可以通過自動化工具運(yùn)行了。
// 引入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 }
]
// 啟動微信開發(fā)者工具
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
// 獲取當(dāng)前頁面實例
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;
}
// 每次操作結(jié)束后,等待 5s,防止頁面跳轉(zhuǎn)過程中,后面的操作找不到頁面
await page.waitFor(5000)
}
// 關(guān)閉 IDE
await miniProgram.close()
})
復(fù)制代碼
這里只是簡單的還原了用戶的操作行為,實際運(yùn)行過程中,還會涉及到網(wǎng)絡(luò)請求和 localstorage 的 mock,這里不再展開講述。同時,我們還可以接入 jest 工具,更加方便用例的編寫。
總結(jié)
看似很難的需求,只要用心去發(fā)掘,總能找到對應(yīng)的解決辦法。另外微信小程序的自動化工具真的有很多坑,遇到問題可以先到小程序社區(qū)去找找,大部分坑都有前人踩過,還有一些一時無法解決的問題只能想其他辦法來規(guī)避。最后祝愿天下無 bug。
相關(guān)案例查看更多
相關(guān)閱讀
- 網(wǎng)站排名
- 云南網(wǎng)站建設(shè)專業(yè)品牌
- 北京小程序開發(fā)
- 網(wǎng)站建設(shè)制作
- 搜索引擎排名
- 云南網(wǎng)站建設(shè)
- 網(wǎng)站建設(shè)報價
- 小程序開發(fā)聯(lián)系方式
- 云南小程序代建
- 云南網(wǎng)站建設(shè)電話
- 云南網(wǎng)站建設(shè)公司
- 云南省建設(shè)廳官方網(wǎng)站
- 網(wǎng)站優(yōu)化
- 旅游網(wǎng)站建設(shè)
- 網(wǎng)站建設(shè)特性
- 云南網(wǎng)站開發(fā)哪家好
- 云南網(wǎng)頁制作
- 小程序被攻擊
- 專業(yè)網(wǎng)站建設(shè)公司
- 汽車報廢管理
- 汽車拆解系統(tǒng)
- 網(wǎng)站建設(shè)服務(wù)
- 人人商城
- 百度快速排名
- 排名
- 網(wǎng)站優(yōu)化公司
- 軟件開發(fā)
- 昆明小程序代建
- 昆明網(wǎng)站制作
- 云南做百度小程序的公司