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