您好,登錄后才能下訂單哦!
這篇文章主要介紹關(guān)于小程序自動(dòng)化測(cè)試的案例分析,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!
背景
近期團(tuán)隊(duì)打算做一個(gè)小程序自動(dòng)化測(cè)試的工具,期望能夠做的業(yè)務(wù)人員操作一遍小程序后,自動(dòng)還原之前的操作路徑,并且捕獲操作過程中發(fā)生的異常,以此來判斷這次發(fā)布時(shí)候會(huì)影響小程序的基礎(chǔ)功能。
上述描述看似簡(jiǎn)單,但是中間還是有些難點(diǎn)的,第一個(gè)難點(diǎn)就是如何在業(yè)務(wù)人員操作小程序的時(shí)候記錄操作路徑,第二個(gè)難點(diǎn)就是如何將記錄的操作路徑進(jìn)行還原。
自動(dòng)化 SDK
如何將操作路徑還原這個(gè)問題,當(dāng)然首選官方提供的 SDK: miniprogram-automator
。
小程序自動(dòng)化 SDK 為開發(fā)者提供了一套通過外部腳本操控小程序的方案,從而實(shí)現(xiàn)小程序自動(dòng)化測(cè)試的目的。通過該 SDK,你可以做到以下事情:
上面的描述都來自官方文檔,建議閱讀后面內(nèi)容之前可以先看看官方文檔 ,當(dāng)然如果之前用過 puppeteer ,基本是無(wú)縫銜接。下面簡(jiǎn)單介紹下 SDK 的使用方式。
// 引入sdk const automator = require('miniprogram-automator') // 啟動(dòng)微信開發(fā)者工具 automator.launch({ // 微信開發(fā)者工具安裝路徑下的 cli 工具 // Windows下為安裝路徑下的 cli.bat // MacOS下為安裝路徑下的 cli cliPath: 'path/to/cli', // 項(xiàng)目地址,即要運(yùn)行的小程序的路徑 projectPath: 'path/to/project', }).then(async miniProgram => { // miniProgram 為 IDE 啟動(dòng)后的實(shí)例 // 啟動(dòng)小程序里的 index 頁(yè)面 const page = await miniProgram.reLaunch('/page/index/index') // 等待 500 ms await page.waitFor(500) // 獲取頁(yè)面元素 const element = await page.$('.main-btn') // 點(diǎn)擊元素 await element.tap() // 關(guān)閉 IDE await miniProgram.close() })
有個(gè)地方需要提醒一下:使用 SDK 之前需要開啟開發(fā)者工具的服務(wù)端口,要不然會(huì)啟動(dòng)失敗。
捕獲用戶行為
有了還原操作路徑的辦法,接下來就要解決記錄操作路徑的難題了。
在小程序中,并不能像 web 中通過事件冒泡的方式在 window 中捕獲所有的事件,好在小程序所以的頁(yè)面和組件都必須通過 Page
、 Component
方法來包裝,所以我們可以改寫這兩個(gè)方法,攔截傳入的方法,并判斷第一個(gè)參數(shù)是否為 event
對(duì)象,以此來捕獲所有的事件。
// 暫存原生方法 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 // 取出第一個(gè)參數(shù) // 判斷是否為 event 對(duì)象 if (evt && evt.target && evt.type) { // 記錄用戶行為 } return method.apply(this, args) } }
這里的代碼只是代理了所有的事件方法,并不能用來還原用戶的行為,要還原用戶行為還必須知道該事件類型是否是需要的,比如點(diǎn)擊、長(zhǎng)按、輸入。
const evtTypes = [ 'tap', // 點(diǎn)擊 'input', // 輸入 'confirm', // 回車 'longpress' // 長(zhǎng)按 ] const hookMethod = (name, method) => { return function(...args) { const [evt] = args // 取出第一個(gè)參數(shù) // 判斷是否為 event 對(duì)象 if ( evt && evt.target && evt.type && evtTypes.includes(evt.type) // 判斷事件類型 ) { // 記錄用戶行為 } return method.apply(this, args) } }
確定事件類型之后,還需要明確點(diǎn)擊的元素到底是哪個(gè),但是小程序里面比較坑的地方就是,event 對(duì)象的 target 屬性中,并沒有元素的類名,但是可以獲取元素的 dataset。
為了準(zhǔn)確的獲取元素,我們需要在構(gòu)建中增加一個(gè)步驟,修改 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>
但是獲取到 class 之后,又會(huì)有另一個(gè)坑,小程序的自動(dòng)化測(cè)試工具并不能直接獲取頁(yè)面里自定義組件中的元素,必須先獲取自定義組件。
<!-- 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 會(huì)得到 null const element = await page.$('.toast-close') element.tap() // Error! // 必須先通過自定義組件的 tagName 找到自定義組件 // 再?gòu)淖远x組件中通過 className 查找對(duì)應(yīng)元素 const element = await page.$('toast .toast-close') element.tap()
所以我們?cè)跇?gòu)建操作的時(shí)候,還需要為元素插入 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" />
現(xiàn)在我們可以繼續(xù)愉快的記錄用戶行為了。
// 記錄用戶行為的數(shù)組 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 // 取出第一個(gè)參數(shù) // 判斷是否為 event 對(duì)象 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事件觸發(fā)時(shí),輸入框的值 // 記錄用戶行為 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) } }
到這里已經(jīng)記錄了用戶所有的點(diǎn)擊、輸入、回車相關(guān)的操作,但是還有一個(gè)滾動(dòng)屏幕的操作還沒記錄。這里可以直接監(jiān)聽 Page 的 onPageScroll。
// 記錄用戶行為的數(shù)組 const actions = []; // 添加用戶行為 const addAction = (type, query, value = '') => { if (type === 'scroll' || type === 'input') { // 如果上一次行為也是滾動(dòng)或輸入,則重置 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) { // 進(jìn)行方法攔截 if (typeof obj[name] === 'function') { params[name] = hookMethod(name, params[name], false) } } const { onPageScroll } = params // 攔截滾動(dòng)事件 params.onPageScroll = function (...args) { const [evt] = args const { scrollTop } = evt addAction('scroll', '', scrollTop) onPageScroll.apply(this, args) } originPage(params) }
這里有個(gè)優(yōu)化點(diǎn),就是滾動(dòng)操作記錄的時(shí)候,可以判斷一下上次操作是否也為滾動(dòng)操作,如果是同一個(gè)操作,則只需要修改一下滾動(dòng)距離即可,以為兩次滾動(dòng)可以一步到位。同理,輸入事件也是,輸入的值也可以一步到位。
還原用戶行為
用戶操作完畢后,可以在控制臺(tái)輸出用戶行為的 json 文本,把 json 文本復(fù)制出來后,就可以通過自動(dòng)化工具運(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 } ] // 啟動(dòng)微信開發(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) { // 計(jì)算兩次操作之間的等待時(shí)間 await page.waitFor(time - prevTime) } // 重置上次操作時(shí)間 prevTime = time // 獲取當(dāng)前頁(yè)面實(shí)例 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,防止頁(yè)面跳轉(zhuǎn)過程中,后面的操作找不到頁(yè)面 await page.waitFor(5000) } // 關(guān)閉 IDE await miniProgram.close() })
這里只是簡(jiǎn)單的還原了用戶的操作行為,實(shí)際運(yùn)行過程中,還會(huì)涉及到網(wǎng)絡(luò)請(qǐng)求和 localstorage 的 mock,這里不再展開講述。同時(shí),我們還可以接入 jest 工具,更加方便用例的編寫。
看似很難的需求,只要用心去發(fā)掘,總能找到對(duì)應(yīng)的解決辦法。另外微信小程序的自動(dòng)化工具真的有很多坑,遇到問題可以先到小程序社區(qū)去找找,大部分坑都有前人踩過,還有一些一時(shí)無(wú)法解決的問題只能想其他辦法來規(guī)避。最后祝愿天下無(wú) bug。
以上是關(guān)于小程序自動(dòng)化測(cè)試的案例分析的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。