京喜前端自動化測試之(zhī)路(小程序篇)
發表時(shí)間:2021-1-11
發布人(rén):融晨科技
浏覽次數:103
如果你已經閱讀過 《京喜前端自動化測試之(zhī)路(一)》,可跳過前言部分閱讀。
前言
京喜(原京東拼購)項目,作爲(wéi / wèi)京東戰略級業務,擁有千萬級别的(de)流量入口。爲(wéi / wèi)了(le/liǎo)保障線上(shàng)業務的(de)穩定運行,每月例行開展前端容災演習,主要(yào / yāo)包含小程序及 H5 版本,要(yào / yāo)求各頁面各模塊在(zài)異常情況下進行适當的(de)降級處理,不(bù)能出(chū)現空窗、樣式錯亂、不(bù)合理的(de)錯誤提示等體驗問題。
容災演習是(shì)一項長期持續的(de)工作,且涉及頁面功能及場景多,人(rén)工的(de)切換場景模拟異常導緻演習效率較低,因此想通過開發自動化測試工具來(lái)提升演習效率,讓容災演習工作随時(shí)可以(yǐ)輕松開展。由于(yú)京喜 H5 和(hé / huò)小程序場景差異比較大(dà),自動化測試分 H5 和(hé / huò)小程序兩部分進行。前期已經分享過 H5 的(de)自動化測試方案 —— 京喜前端自動化測試之(zhī)路(一)
,本文則主要(yào / yāo)講述小程序版的(de)自動化測試方案。
綜上(shàng)所述,我們希望京喜小程序自動化測試工具可以(yǐ)提供以(yǐ)下功能:
- 訪問目标頁面,對頁面進行截圖;
- 模拟用戶點擊、滑動頁面操作;
- 網絡攔截、模拟異常情況(接口響應碼 500、接口返回數據異常);
- 操作緩存數據(模拟有無緩存的(de)場景等)。
小程序自動化 SDK
聊到(dào)小程序的(de)自動化工具,微信官方爲(wéi / wèi)開發者提供了(le/liǎo)一套小程序自動化 SDK —— miniprogram-automator , 我們不(bù)需要(yào / yāo)關注技術選型,可直接使用。
小程序自動化 SDK 爲(wéi / wèi)開發者提供了(le/liǎo)一套通過外部腳本操控小程序的(de)方案,從而(ér)實現小程序自動化測試的(de)目的(de)。
如果你之(zhī)前使用過 Selenium WebDriver 或者 Puppeteer,那你可以(yǐ)很容易快速上(shàng)手。小程序自動化 SDK 與它們的(de)工作原理是(shì)類似的(de),主要(yào / yāo)區别在(zài)于(yú)控制對象由浏覽器換成了(le/liǎo)小程序。
特性
通過該 SDK,你可以(yǐ)做到(dào)以(yǐ)下事情:
- 控制小程序跳轉到(dào)指定頁面
- 獲取小程序頁面數據
- 獲取小程序頁面元素狀态
- 觸發小程序元素綁定事件
- 往 AppService 注入代碼片段
- 調用 wx 對象上(shàng)任意接口
- ...
示例
const automator = require('miniprogram-automator')
automator
.launch({
cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli', // 工具 cli 位置(絕對路徑)
projectPath: 'path/to/project', // 項目文件地(dì / de)址(絕對路徑)
})
.then(async miniProgram => {
const page = await miniProgram.reLaunch('/pages/index/index')
await page.waitFor(500)
const element = await page.$('.banner')
console.log(await element.attribute('class'))
await element.tap()
await miniProgram.close()
})
複制代碼
綜上(shàng)所述,我們選擇使用官方維護的(de) SDK —— miniprogram-automator
開發小程序的(de)自動化測試工具,通過 SDK 提供的(de)一系列 API ,實現訪問目标頁面、模拟異常場景、生成截圖的(de)過程自動化。最後再通過人(rén)工比對截圖,判斷頁面降級處理是(shì)否符合預預期、用戶體驗是(shì)否友好。
實現方案
原來(lái)的(de)容災演習過程:
小程序的(de)通信方式改成 HTTPS ,通過 Whistle 對接口返回進行修改來(lái)模拟異常情況,驗證各頁面各模塊的(de)降級處理符合預期。
現階段的(de)容災演習自動化方案:
我們将容災演習過程分爲(wéi / wèi)自動化流程
和(hé / huò)人(rén)工操作
兩部分。
自動化流程:
- 啓動微信開發者工具(開發版);
- 訪問目标頁面,模拟用戶點擊、滑動等行爲(wéi / wèi);
- 模拟異常場景:攔截網絡請求,修改接口返回數據(接口返回 500、異常數據等);
- 生成截圖。
人(rén)工操作:
自動化腳本執行完畢後,人(rén)工比對各個(gè)場景的(de)截圖,判斷是(shì)否符合預期。
方案流程圖:
開發實錄
快速創建測試用例
爲(wéi / wèi)了(le/liǎo)提高測試腳本的(de)可維護性、擴展性,我們将測試用例的(de)信息都配置到(dào) JSON 文件中,這(zhè)樣編寫測試腳本的(de)時(shí)候,我們隻需關注測試流程的(de)實現。
測試用例 JSON 數據配置包括公用數據(global)
和(hé / huò)私有數據
:
公用數據(global)
:各測試用例都需要(yào / yāo)用到(dào)的(de)數據,如:模拟訪問的(de)目标頁面地(dì / de)址、名字、描述、設備類型等。
私有數據
: 各測試用例特定的(de)數據,如測試模塊信息、api 地(dì / de)址、測試場景、預期結果、截圖名字等數據。
{
"global": {
"url": "/pages/index/index",
"pageName": "index",
"pageDesc": "首頁",
"device": "iPhone X"
},
"homePageApi": {
"id": 1,
"module": "home_page_api",
"moduleDesc": "首頁主接口",
"api": "https://xxx",
"operation": "模拟響應碼 500",
"expectRules": [
"1. 有緩存數據,顯示容災兜底數據",
"2. 請求容災接口,顯示容災兜底數據",
"3. 容災接口異常,顯示信異常息、刷新按鈕",
"4. 恢複網絡,點擊刷新按鈕,顯示正常數據"
],
"screenshot": [
{
"name": "normal",
"desc": "正常場景"
},
{
"name": "500_cache",
"desc": "有緩存-主接口返回500"
},
{
"name": "500_no_cache",
"desc": "無緩存-主接口返回500-容災兜底數據"
},
{
"name": "500_no_cache_500_disaster",
"desc": "無緩存-主接口返回500-容災兜底接口返回500"
},
{
"name": "500_no_cache_recover",
"desc": "無緩存-返回500-恢複網絡"
}
]
},
…
}
複制代碼
編寫測試腳本
我們以(yǐ)京喜首頁主接口的(de)測試用例爲(wéi / wèi)例子(zǐ),通過模拟主接口返回 500 響應碼的(de)異常場景,驗證主接口的(de)異常處理機制是(shì)否完善、用戶體驗是(shì)否友好。
預期效果:
- 主接口異常,有緩存數據,顯示緩存數據
- 主接口異常,無緩存數據,則請求容災接口,顯示容災兜底數據
- 主接口、容災接口異常,無緩存數據,顯示信異常息、刷新按鈕
- 恢複網絡,點擊刷新按鈕,顯示正常數據
測試流程:
場景實現:
根據測試流程以(yǐ)及配置的(de)測試用例信息,編寫測試腳本,模拟測試用例場景:
- 訪問頁面
const miniProgram = await automator.launch({
cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli', // 開發者工具命令行工具(絕對路徑)
projectPath: 'jx_project', // 項目地(dì / de)址(絕對路徑)
})
await miniProgram.reLaunch('/pages/index/index')
複制代碼
- 生成截圖
await miniProgram.screenshot({
path: 'jx_weapp_index_home_page_500.png'
})
複制代碼
- 模拟異常數據
const getMockData = http://www.wxapp-union.com/(url, mockType, mockValue) => {
const result = {
data: 'test',
cookies: [],
header: {},
statusCode: 200,
}
switch (mockType) {
case 'data':
result.data = http://www.wxapp-union.com/getMockResponse(url, mockValue) // 修改返回數據
break
case 'cookies':
result.cookies = mockValue // 修改返回數據
break
case 'header':
result.header = mockValue // 修改返回響應頭
break
case 'statusCode':
result.statusCode = mockValue // 修改返回響應頭
break
}
return {
rule: url,
result
}
}
// 修改本地(dì / de)存儲數據
const mockValue = http://www.wxapp-union.com/{
data: {
modules: [{
tpl:'3000',
content: []
}]
}
}
const mockData = http://www.wxapp-union.com/[
getMockData(api1, 'statusCode', 500), // 模拟接口返回 500
getMockData(api2, 'data', mockValue) // 模拟接口返回異常數據
...
]
複制代碼
- 攔截接口請求,修改返回數據
const interceptAPI = async (miniProgram, url, mockData) => {
try {
await miniProgram.mockWxMethod(
'request',
function(obj, data) { // 處理返回函數
for (let i = 0, len = data.length; i < len; i++) {
const item = data[i]
// 命中規則的(de)返回 mockData
if (obj.url.indexOf(item.rule) > -1) {
return item.result
}
}
// 沒命中規則的(de)真實訪問後台
return new Promise(resolve => {
obj.success = res => resolve(res)
obj.fail = res => resolve(res)
/ origin 指向原始方法
this.origin(obj)
})
},
mockData, // 傳入 mock 數據
)
} catch (e) {
console.error(`攔截【${url}】API報錯`)
console.error(e)
}
}
await interceptAPI(interceptAPI, url, mockData)
複制代碼
miniProgram.mockWxMethod
:覆蓋 wx 對象上(shàng)指定方法的(de)調用結果。利用該 API,可以(yǐ)覆蓋 wx.request API,攔截網絡請求,修改返回數據。- 目前是(shì)本地(dì / de)存儲一份接口返回的(de) JSON 數據,通過修改本地(dì / de)的(de) JSON 數據生成 mockData。若需要(yào / yāo)修改接口實時(shí)返回的(de)數據,可在(zài)
obj.success
中獲取實時(shí)數據并修改。
- 清除緩存
try {
await miniProgram.callWxMethod('clearStorage')
} catch (e) {
await console.log(`清除緩存報錯: `)
await console.log(e)
}
複制代碼
- 點擊刷新按鈕
const page = await miniProgram.currentPage()
const $refreshBtn = await page.$('.page-error__refresh-btn') // 同 WXSS,僅支持部分 CSS 選擇器
await $refreshBtn.tap()
複制代碼
- 取消攔截,恢複網絡
const cancelInterceptAPI = async (miniProgram) => {
try {
await miniProgram.restoreWxMethod('request') // 重置 wx.request ,消除 mockWxMethod 調用的(de)影響。
} catch (e) {
console.error(`取消攔截【${url}】API報錯`)
console.error(e)
}
}
await cancelInterceptAPI(miniProgram)
複制代碼
啓動自動化測試
由于(yú)第一階段的(de)測試工具尚未平台化,先通過在(zài)終端輸入命令行,運行腳本的(de)方式,啓動自動化測試。
在(zài)項目的(de) package.json 文件中,使用 scripts 字段定義腳本命令:
"scripts": {
"start": "node pages/index/index.js"
},
複制代碼
運行環境:
- 安裝 Node.js 并且版本大(dà)于(yú) 8.0
- 基礎庫版本爲(wéi / wèi) 2.7.3 及以(yǐ)上(shàng)
- 開發者工具版本爲(wéi / wèi) 1.02.1907232 及以(yǐ)上(shàng)
運行:
在(zài)終端切入到(dào)項目根目錄路徑,輸入以(yǐ)下命令行,就(jiù)可以(yǐ)啓動測試工具,運行測試腳本。
$ npm run start
複制代碼
測試結果
運行腳本示例:
使用 SDK,你必須知道(dào) Shadow DOM
當我們想控制小程序頁面時(shí),需獲取頁面實例 page,利用 page 提供的(de)方法控制頁面内的(de)元素。
比如,當我們想點擊頁面中搜索框時(shí),我們一般會這(zhè)麽做:
const page = await miniProgram.currentPage()
const $searchBar = await page.$('search-bar')
await $searchBar.tap()
複制代碼
但這(zhè)樣真的(de)可行嗎?答案是(shì):
試試就(jiù)知道(dào)了(le/liǎo)。
運行這(zhè)段測試腳本後生成的(de)截圖:
我們得到(dào)的(de)結果是(shì):根本沒有觸發點擊事件。
Shadow DOM:
它是(shì) HTML 的(de)一個(gè)規範,它允許在(zài)文檔( document )渲染時(shí)插入一顆DOM元素子(zǐ)樹,但是(shì)這(zhè)個(gè)子(zǐ)樹不(bù)在(zài)主 DOM 樹中。
它允許浏覽器開發者封裝自己的(de) HTML 标簽、css 樣式和(hé / huò)特定的(de) javascript 代碼、同時(shí)開發人(rén)員也(yě)可以(yǐ)創建類似 <input>、<video>、<audio>
等、這(zhè)樣的(de)自定義的(de)一級标簽。創建這(zhè)些标簽内容相關的(de) API,可以(yǐ)被叫做 Web Component。
Shadow DOM 的(de)關鍵所在(zài),它可以(yǐ)将一個(gè)隐藏的(de)、獨立的(de) DOM 附加到(dào)一個(gè)元素上(shàng)。
Shadow host:
一個(gè)常規 DOM 節點,Shadow DOM 會被附加到(dào)這(zhè)個(gè)節點上(shàng)。它是(shì) Shadow DOM 的(de)一個(gè)宿主元素。比如:<input />、<audio>、<video>
标簽,就(jiù)是(shì) Shadow DOM 的(de)宿主元素。Shadow tree:
Shadow DOM 内部的(de) DOM 樹。Shadow root:
Shadow DOM 的(de)根節點。通過createShadowRoot
返回的(de)文檔片段被稱爲(wéi / wèi) shadow-root , 它和(hé / huò)它的(de)後代元素,都會對用戶隐藏。
回到(dào)我們剛剛的(de)問題:
由于(yú)小程序使用了(le/liǎo) Shadow DOM,因此我們不(bù)能直接通過 page 實例獲取到(dào)搜索框真實 DOM。我們看到(dào)的(de)頁面中渲染的(de)搜索框,實際上(shàng)是(shì)一個(gè) Shadow DOM。因此,我們必須先獲取到(dào)搜索框 Shadow DOM 的(de)宿主元素,并通過宿主元素獲取到(dào)搜索框真實的(de) DOM,最後觸發真實 DOM 的(de)點擊事件。
const page = await miniProgram.currentPage()
const $searchBarShadow = await page.$('search-bar')
const $searchBar = await $searchBarShadow.$('.search-bar')
const { height } = await $searchBar.size()
複制代碼
運行這(zhè)段測試腳本後生成的(de)截圖:
從截圖可以(yǐ)看到(dào),觸發了(le/liǎo)搜索框的(de)點擊事件。
更多測試場景實現
1. 下拉刷新
const pullDownRefresh = async (miniProgram) => {
try {
await miniProgram.callWxMethod('startPullDownRefresh')
} catch (e) {
console.error('下拉刷新操作失敗')
console.error(e)
}
}
複制代碼
2. 滾動到(dào)指定 DOM
const page = await miniProgram.currentPage() // 獲取頁面實例
const $recommendTabShadow = await page.$('recommend-tab') // 獲取Shadow DOM
const $recommendTab = await $recommendTabShadow.$('.recommend') // 獲取真實 DOM
const { top } = await $recommendTab.offset() // 獲取DOM 定位
await miniProgram.pageScrollTo(top) // 滾動到(dào)指定DOM
複制代碼
3. 事件
- 日志打印;
- 監聽頁面崩潰事件
// 日志打印時(shí)觸發
miniProgram.on('console', msg => {
console.log(msg.type, msg.args)
})
})
// 頁面 JS 出(chū)錯時(shí)觸發
page.on('error', (e) => {
console.log(e)
})
複制代碼
結語
第一階段的(de)小程序自動化測試之(zhī)路告一段落。和(hé / huò) H5 自動化測試一樣,容災演習已實現了(le/liǎo)半自動化,可通過在(zài)終端運行測試腳本,模拟異常場景自動生成截圖,再配合人(rén)工比對截圖操作,判斷演習結果是(shì)否符合預期。目前已投入到(dào)每個(gè)月的(de)容災演習中使用。
由于(yú) H5 和(hé / huò)小程序的(de)差異比較大(dà),第一階段的(de)自動化測試分兩端進行,測試腳本語法也(yě)是(shì)截然不(bù)同,需要(yào / yāo)同時(shí)維護兩套測試工具。爲(wéi / wèi)了(le/liǎo)降低維護成本,提升測試腳本的(de)開發效率,我們正在(zài)研發第二階段的(de)自動化測試工具,一套代碼支持兩端測試,目前已經進入内測階段。更多彩蛋,敬請期待第二階段自動化測試工具——多端自動化測試 SDK 。
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆号(AOTULabs),不(bù)定時(shí)推送文章