做者:阿翔javascript
若是你已經閱讀過 《京喜前端自動化測試之路(一)》,可跳過前言部分閱讀。css
京喜(原京東拼購)項目,做爲京東戰略級業務,擁有千萬級別的流量入口。爲了保障線上業務的穩定運行,每個月例行開展前端容災演習,主要包含小程序及 H5 版本,要求各頁面各模塊在異常狀況下進行適當的降級處理,不能出現空窗、樣式錯亂、不合理的錯誤提示等體驗問題。前端
容災演習是一項長期持續的工做,且涉及頁面功能及場景多,人工的切換場景模擬異常致使演習效率較低,所以想經過開發自動化測試工具來提高演習效率,讓容災演習工做隨時能夠輕鬆開展。因爲京喜 H5 和小程序場景差別比較大,自動化測試分 H5 和小程序兩部分進行。前期已經分享過 H5 的自動化測試方案 —— 京喜前端自動化測試之路(一)
,本文則主要講述小程序版的自動化測試方案。java
綜上所述,咱們但願京喜小程序自動化測試工具能夠提供如下功能:node
聊到小程序的自動化工具,微信官方爲開發者提供了一套小程序自動化 SDK —— miniprogram-automator , 咱們不須要關注技術選型,可直接使用。web
小程序自動化 SDK 爲開發者提供了一套經過外部腳本操控小程序的方案,從而實現小程序自動化測試的目的。npm
若是你以前使用過 Selenium WebDriver 或者 Puppeteer,那你能夠很容易快速上手。小程序自動化 SDK 與它們的工做原理是相似的,主要區別在於控制對象由瀏覽器換成了小程序。json
特性小程序
經過該 SDK,你能夠作到如下事情:api
示例
const automator = require('miniprogram-automator') automator .launch({ cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli', // 工具 cli 位置(絕對路徑) projectPath: 'path/to/project', // 項目文件地址(絕對路徑) }) .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() })
綜上所述,咱們選擇使用官方維護的 SDK —— miniprogram-automator
開發小程序的自動化測試工具,經過 SDK 提供的一系列 API ,實現訪問目標頁面、模擬異常場景、生成截圖的過程自動化。最後再經過人工比對截圖,判斷頁面降級處理是否符合預預期、用戶體驗是否友好。
原來的容災演習過程:
小程序的通訊方式改爲 HTTPS ,經過 Whistle 對接口返回進行修改來模擬異常狀況,驗證各頁面各模塊的降級處理符合預期。
現階段的容災演習自動化方案:
咱們將容災演習過程分爲自動化流程
和人工操做
兩部分。
自動化流程:
人工操做:
自動化腳本執行完畢後,人工比對各個場景的截圖,判斷是否符合預期。
方案流程圖:
爲了提升測試腳本的可維護性、擴展性,咱們將測試用例的信息都配置到 JSON 文件中,這樣編寫測試腳本的時候,咱們只需關注測試流程的實現。
測試用例 JSON 數據配置包括公用數據(global)
和私有數據
:
公用數據(global)
:各測試用例都須要用到的數據,如:模擬訪問的目標頁面地址、名字、描述、設備類型等。
私有數據
: 各測試用例特定的數據,如測試模塊信息、api 地址、測試場景、預期結果、截圖名字等數據。
{ "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-恢復網絡" } ] }, … }
咱們以京喜首頁主接口的測試用例爲例子,經過模擬主接口返回 500 響應碼的異常場景,驗證主接口的異常處理機制是否完善、用戶體驗是否友好。
預期效果:
測試流程:
場景實現:
根據測試流程以及配置的測試用例信息,編寫測試腳本,模擬測試用例場景:
const miniProgram = await automator.launch({ cliPath: '/Applications/wechatwebdevtools.app/Contents/MacOS/cli', // 開發者工具命令行工具(絕對路徑) projectPath: 'jx_project', // 項目地址(絕對路徑) }) await miniProgram.reLaunch('/pages/index/index')
await miniProgram.screenshot({ path: 'jx_weapp_index_home_page_500.png' })
const getMockData = (url, mockType, mockValue) => { const result = { data: 'test', cookies: [], header: {}, statusCode: 200, } switch (mockType) { case 'data': result.data = 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 } } // 修改本地存儲數據 const mockValue = { data: { modules: [{ tpl:'3000', content: [] }] } } const mockData = [ 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] // 命中規則的返回 mockData if (obj.url.indexOf(item.rule) > -1) { return item.result } } // 沒命中規則的真實訪問後臺 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 對象上指定方法的調用結果。利用該 API,能夠覆蓋 wx.request API,攔截網絡請求,修改返回數據。obj.success
中獲取實時數據並修改。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 調用的影響。 } catch (e) { console.error(`取消攔截【${url}】API報錯`) console.error(e) } } await cancelInterceptAPI(miniProgram)
因爲第一階段的測試工具還沒有平臺化,先經過在終端輸入命令行,運行腳本的方式,啓動自動化測試。
在項目的 package.json 文件中,使用 scripts 字段定義腳本命令:
"scripts": { "start": "node pages/index/index.js" },
運行環境:
運行:
在終端切入到項目根目錄路徑,輸入如下命令行,就能夠啓動測試工具,運行測試腳本。
$ npm run start
人工比對截圖結果:
運行腳本示例:
當咱們想控制小程序頁面時,需獲取頁面實例 page,利用 page 提供的方法控制頁面內的元素。
好比,當咱們想點擊頁面中搜索框時,咱們通常會這麼作:
const page = await miniProgram.currentPage() const $searchBar = await page.$('search-bar') await $searchBar.tap()
但這樣真的可行嗎?答案是:
試試就知道了。
運行這段測試腳本後生成的截圖:
咱們獲得的結果是:根本沒有觸發點擊事件。
Shadow DOM:
它是 HTML 的一個規範,它容許在文檔( document )渲染時插入一顆DOM元素子樹,可是這個子樹不在主 DOM 樹中。
它容許瀏覽器開發者封裝本身的 HTML 標籤、css 樣式和特定的 javascript 代碼、同時開發人員也能夠建立相似 <input>、<video>、<audio>
等、這樣的自定義的一級標籤。建立這些標籤內容相關的 API,能夠被叫作 Web Component。
Shadow DOM 的關鍵所在,它能夠將一個隱藏的、獨立的 DOM 附加到一個元素上。
Shadow host:
一個常規 DOM 節點,Shadow DOM 會被附加到這個節點上。它是 Shadow DOM 的一個宿主元素。好比:<input />、<audio>、<video>
標籤,就是 Shadow DOM 的宿主元素。Shadow tree:
Shadow DOM 內部的 DOM 樹。Shadow root:
Shadow DOM 的根節點。經過 createShadowRoot
返回的文檔片斷被稱爲 shadow-root , 它和它的後代元素,都會對用戶隱藏。回到咱們剛剛的問題:
因爲小程序使用了 Shadow DOM,所以咱們不能直接經過 page 實例獲取到搜索框真實 DOM。咱們看到的頁面中渲染的搜索框,其實是一個 Shadow DOM。所以,咱們必須先獲取到搜索框 Shadow DOM 的宿主元素,並經過宿主元素獲取到搜索框真實的 DOM,最後觸發真實 DOM 的點擊事件。
const page = await miniProgram.currentPage() const $searchBarShadow = await page.$('search-bar') const $searchBar = await $searchBarShadow.$('.search-bar') const { height } = await $searchBar.size()
運行這段測試腳本後生成的截圖:
從截圖能夠看到,觸發了搜索框的點擊事件。
1. 下拉刷新
const pullDownRefresh = async (miniProgram) => { try { await miniProgram.callWxMethod('startPullDownRefresh') } catch (e) { console.error('下拉刷新操做失敗') console.error(e) } }
2. 滾動到指定 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) // 滾動到指定DOM
3. 事件
// 日誌打印時觸發 miniProgram.on('console', msg => { console.log(msg.type, msg.args) }) }) // 頁面 JS 出錯時觸發 page.on('error', (e) => { console.log(e) })
第一階段的小程序自動化測試之路告一段落。和 H5 自動化測試同樣,容災演習已實現了半自動化,可經過在終端運行測試腳本,模擬異常場景自動生成截圖,再配合人工比對截圖操做,判斷演習結果是否符合預期。目前已投入到每月的容災演習中使用。
因爲 H5 和小程序的差別比較大,第一階段的自動化測試分兩端進行,測試腳本語法也是大相徑庭,須要同時維護兩套測試工具。爲了下降維護成本,提高測試腳本的開發效率,咱們正在研發第二階段的自動化測試工具,一套代碼支持兩端測試,目前已經進入內測階段。更多彩蛋,敬請期待第二階段自動化測試工具——多端自動化測試 SDK 。
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章: