本文做者:IMWeb IMWeb團隊 原文出處:IMWeb社區 未經贊成,禁止轉載javascript
微信小程序生態日益完善,不少小程序項目頁面愈來愈多,結構愈來愈複雜,業務邏輯也更加多樣。以騰訊課堂小程序爲例,目前騰訊課堂小程序部分頁面結構和不一樣業務場景下的表現以下圖所示:html
能夠看到在覈心功能上主要頁面對於不一樣業務場景有衆多不一樣的表現,所以在開發與發佈的過程當中須要手動驗證大量測試用例以保證小程序按預期表現運行,善於利用工具的程序員固然會想:java
這種重複的工做能不能交給程序自動進行呢?node
web開發中對於這類測試問題已經有了不少自動化解決方案好比Selenium、Puppeteer,思路大致相同,都是讓瀏覽器按照指定順序自動在頁面上完成點擊、輸入等操做,再將操做後的頁面表現與想要獲得的結果進行比較獲得測試結論(斷言)。那小程序中有沒有一種方案可以按照這種思路實現自動化操做並提供頁面信息用於斷言呢?爲了微信底層安全考慮,小程序環境一直比較封閉,留給開發者操做的餘地很小,自動化操做基本沒法實現,但5月底出現了miniprogram-automator工具,給了小程序開發者但願。程序員
基於miniprogram-automator的文檔描述簡單總結一下,當經過命令打開開發版微信開發者工具的自動化接口並鏈接自動化接口後,此工具可提供如下能力:web
因此小程序自動化控制的實現依賴於開發版小程序開發者工具以及miniprogram-automator工具。小程序開發者工具命令行用來打開指定自動化操做服務端口。(開發者工具版本需高於v1.02.1906042)。miniprogram-automator工具用來操做開發者工具中運行的小程序並獲取所需的信息。對於測試需求能夠結合jest框架進行測試用例的組織和斷言。npm
很少廢話,看完文檔用一下:小程序
Ø 調用開發者工具命令行打開項目與指定自動化操做服務端口微信小程序
PS D:\programs\內測\微信web開發者工具> ./cli.bat --auto D:\weApp\testMiniprogram --auto-port 9420 Initializing... idePortFile: C:\Users\billcui\AppData\Local\微信開發者工具\User Data\Default\.ide starting ide... IDE server has started, listening on http://127.0.0.1:35510 initialization finished Open project with automation enabled success D:\weApp\testMiniprogram 複製代碼
這一行命令須要注意的有:瀏覽器
Open project with automation enabled success D:\weApp\testMiniprogram
這行提示纔算是成功打開了自動化端口(9420)。命令運行成功後,開發者工具會自動打開項目,並彈出提示
Ø npm i miniprogram-automator --save-dev
安裝SDK,建立test.js,代碼中引入miniprogram-automator工具,鏈接自動化操做端口
const automator = require('miniprogram-automator'); const miniProgram = automator.connect({ wsEndpoint: 'ws://localhost:9420', }) 複製代碼
Ø 利用miniprogram-automator提供的接口操做小程序從首頁重啓並進行相關操做
const automator = require('miniprogram-automator'); const miniProgram = automator.connect({ wsEndpoint: 'ws://localhost:9420', }).then(async miniProgram => { // 從首頁重啓 const page = await miniProgram.reLaunch('/pages/index/index'); // 從頁面獲取bottom-button組件 const button = await page.$('bottom-button'); // 打印出button的wxml信息 console.log(await button.wxml()); }).catch(e => { console.log('catch a error', e); }); 複製代碼
Ø 利用miniprogram-automator獲取操做後頁面相關信息,利用jest進行組織和斷言
// index.spec.js const automator = require('miniprogram-automator'); describe('課堂小程序自動化測試', () => { let miniProgram; // 運行測試前調用 beforeAll(async () => { miniProgram = await automator.connect({ wsEndpoint: 'ws://localhost:9420', }); }); // 運行測試後調用 afterAll(() => { miniProgram.disconnect(); }); // 測試內容 it('nohost檢測', async () => { const page = await miniProgram.reLaunch('/pages/index/index'); const nohostButton = await page.$('nohost'); expect(nohostButton).toBeNull(); }); }); 複製代碼
運行jest index.spec.js
, 若是頁面中不存在nohost組件則測試經過,結果如圖所示:
騰訊課堂微信小程序引入自動化測試主要是爲了解決開發、預發佈環境、正式環境須要反覆屢次打開用例課程頁面,操做繁瑣,耗費大量人力的問題。針對課堂小程序checklist,儘量利用自動化測試程序完成測試驗證,減小手動操做,也能夠避免人爲檢測的遺漏。
利用miniprogram-automator工具和jest框架,自動化測試主要能力爲按照指定順序模擬打開指定頁面、點擊、滾動等操做和設置page的data渲染數據,而後對特定的頁面結構、數據、組件屬性等信息進行斷言,判斷是否符合預期。
下面以騰訊課堂微信小程序的課程詳情頁爲例來詳細說明在實際項目中如何實現自動化測試:
課程詳情頁的UI主要分爲視頻部分,詳情部分以及底部的購買按鈕,未購買課程時付費課程詳情頁表現以下:
假如對於未購買的無優惠活動的付費課程詳情頁的測試目標以下:
實現這個測試,在x.spec.js
文件中首先須要要按照上文的步驟引入miniprogram-automator,在beforeAll中鏈接已經打開自動化端口的微信小程序項目。(這裏再也不重複代碼,見上一章)下面直接看測試內容的代碼。
// 打開頁面,經過url傳參 const page = await miniProgram.reLaunch(`/pages/course/course?cid=${commonPayCid}`); // 獲取按鈕組件信息 const basicApplyButton = await page.$('.basic--buy'); // 判斷按鈕顯示內容 expect(await basicApplyButton.wxml()).toContain('當即購買'); // 模擬點擊按鈕 await basicApplyButton.tap(); // 等待頁面跳轉 await page.waitFor(1500); // 獲取當前頁面路徑 const currentPage = await miniProgram.currentPage(); // 判斷跳轉後路徑是否正確 expect(currentPage.path).toContain('pages/order/order'); // 跳轉回來 await miniProgram.navigateBack(); 複製代碼
目前miniprogram-automator提供了兩種方法獲取到頁面中的組件:page.$()
和page.?()
通過實驗發現二者的selector支持經過組件名和類名選擇組件,但對於自定義組件內部的結構,就不能直接這樣拿到了。
課程詳情頁的底部按鈕實際上是一個自定義組件,而且還嵌套了子自定義組件,咱們看一下底部按鈕的wxml結構:
紅色框框就是想要獲取的目標,嘗試一下直接經過page.$('.bottom-btn')
或page.$('.buy')
返回的都是undefined,那怎麼獲取呢?咱們先來看看bottom-button內部是什麼樣子的。
const basicApplyButton = await page.$('bottom-button'); console.log(await basicApplyButton.wxml()); 複製代碼
獲取bottom-button並打印它的wxml字符串看一下:
// 輸出其實是字符串,爲了方便顯示格式化了一下 <view class="bottom-button--bottom-button-space" wx:nodeid="17"> <view class="bottom-button--bottom-button-wrapper" wx:nodeid="261"> <basic is="components/discount-button/components/basic/basic" wx:nodeid="262"> <view wx:nodeid="263"> <view class="basic--bottom-button-container" wx:nodeid="264"> <view class="basic--bottom-btn basic--buy" wx:nodeid="265">當即購買</view> </view> </view> </basic> </view> </view> 複製代碼
發現了什麼!小程序實際運行時,自定義組件內部的類名都加上了組件名前綴,再試試page.$('.basic--buy')
發現果真成功獲取到了,因此雖然表面上miniprogram-automator只能操做和獲取page中的內容,但自定義組件內部的結構實際上也是以某種方式存在於page中的。
接下來看一下跳轉,能夠直接獲取到對應組件後調用.tap()
方法來模擬點擊,這裏須要注意的是,因爲微信小程序開發者工具中點擊打開新頁面耗時較長,須要等待頁面加載一會,否則接下來獲取當前頁面路徑的時候頁面還沒跳轉過去就拿不到不到新頁面路徑了。等待的時長能夠根據經驗給個稍大的比較安全的值。
const player_video = await tapTcplayer(page, '.player-task'); expect(await player_video.wxml()).toContain('video-current-time'); // 試學 複製代碼
因爲微信開發者工具的限制,雲點播會降級爲tcplayer播放,tcplayer內部的核心組件實際上是<video>
組件,wxml結構以下:
如何判斷視頻是否成功播放呢?
咱們先按照上面的方法獲取播放成功的video組件的wxml字符串看看
"<video class="component-video-video--player_video" controls="" danmu-list="[]" initial-time="0" object-fit="contain" poster="https://10.url.cn/qqc..." src="http://113.96.98.148/vedu.tc.qq.com/AtmkzyWCuq..." autoplay="" wx:nodeid="446"><div class="video-container" wx:nodeid="447"><div class="video-bar full" style="opacity: 1;" wx:nodeid="457"><div class="video-controls" wx:nodeid="458"><div class="video-control-button pause" wx:nodeid="459"><div parse-text-content="" class="video-current-time" wx:nodeid="460">00:02<div class="video-progress-container" wx:nodeid="462"><div class="video-progress" wx:nodeid="463"><div style="left: -21px;" class="video-ball" wx:nodeid="464"><div class="video-inner" wx:nodeid="465"><div parse-text-content="" class="video-duration" wx:nodeid="466">06:09<div class="video-fullscreen" wx:nodeid="468"><div style="z-index: -9999" class="video-danmu" wx:nodeid="453"></video>" 複製代碼
驚了!原生<video>
組件內部居然是<div>
,咱們還能夠注意到一個關鍵的class: video-current-time 內部數值爲00:02,這不是當前播放進度嗎?恰好能夠用來判斷視頻有沒有播放成功,就是它了!
對比發現播放失敗時根本不會出現class爲video-current-time的div,因此直接用是否包含video-current-time來判斷了。
點擊非試看課程時,沒法播放視頻。因爲不播放視頻時頁面中只顯示cover封面圖,不attach <video>
組件,因此直接用獲取視頻組件的結果進行toBeNull()判斷便可。結合上面全部的代碼以下:
async function tapTcplayer(page, className = '.task-item') { const taskItem = await page.$(className); await taskItem.tap(); await page.waitFor(3000); const playercover = await page.$('.player-cover'); const player_video = await playercover.$('.component-video-video--player_video'); return player_video; } it('付費課程詳情頁按鈕顯示、跳轉、點播、試學功能測試', async () => { const page = await miniProgram.reLaunch(`/pages/course/course?cid=${commonPayCid}`); const basicApplyButton = await page.$('.basic--buy'); expect(await basicApplyButton.wxml()).toContain('當即購買'); // 按鈕顯示 await basicApplyButton.tap(); await page.waitFor(1500); const currentPage = await miniProgram.currentPage(); expect(currentPage.path).toContain('pages/order/order'); await miniProgram.navigateBack(); const player_video = await tapTcplayer(page); expect(player_video).toBeNull(); // 未報名不能播放視頻 const player_video_new = await tapTcplayer(page, '.player-task'); expect(await player_video_new.wxml()).toContain('current'); // 試學 }, 20000); 複製代碼
能夠看到實際上先測試了播放課程功能,再測試了試學功能,這是爲何呢?
這是一個坑:因爲播放課程失敗時會有showModel彈窗提示,這個彈窗是不在wxml結構中的,沒法用自動化控制工具點擊關閉,實際測試中這個彈窗會阻塞下一個測試項的第一步:頁面跳轉,致使下一個測試項直接打不開頁面致使失敗,只能等待一段時間再跳轉,因此直接把彈窗放在測試試學功能以前,就不會影響下一個測試項了。
還有一個須要注意的地方,在項目中,點擊播放後5秒不觸發進度刷新的方法就會上報視頻播放失敗,實際測試發現通常3秒便可正常播放,因此只等待3秒,3秒後未成功播放的視爲播放失敗。
最後,jest默認一個測試項的時長不能大於5秒,這項測試既有頁面跳轉又有視頻播放,明顯會超出5秒的限制,實際耗時約爲15秒左右,因此修改時長限制爲20000毫秒。
運行測試腳本結果以下:
目前實現的測試功能以下:
checklist中功能測試的完成狀況以下:完成度爲65%
review點 | 自動化測試 | 備註 |
---|---|---|
是否去除nohost插件 | 支持 | |
首頁是否正常顯示 | 支持 | |
pc首頁小程序登錄是否正常 | 暫不 | 信息受權沒法自動完成 |
安卓支付能力是否正常 | 暫不 | webview內部沒法獲取信息 |
分類頁是否正常顯示 | 支持 | |
是否能夠正常登錄 | 暫不 | 信息受權沒法自動完成 |
課程表是否正常展現,學習進度/直播狀態是否正常顯示 | 支持 | 待完善 |
課程詳情頁是否能夠正常展現 | 支持 | |
掃碼/分享是否正常喚起小程序 | 暫不 | 開發者工具不支持 |
付費課直播是否能夠正常播放(上雲跟騰訊視頻) | 暫不 | 開發者工具不支持直播 |
免費課直播是否能夠正常播放(上雲跟騰訊視頻) | 暫不 | 開發者工具不支持直播 |
免費課錄播是否能夠正常播放(上雲跟騰訊視頻) | 部分支持 | 開發者工具降級到tcplayer |
付費課錄播是否能夠正常播放(上雲跟騰訊視頻) | 部分支持 | 開發者工具降級到tcplayer |
試學任務是否能夠正常播放 | 支持 | |
詳情頁視頻是否正常播放 | 支持 | |
營銷工具相關顯示是否正常 | 支持 | |
是否能正常完成支付邏輯 | 暫不 | webview內部沒法獲取信息 |
類目篩選是否正常 | 支持 | 待完善 |
是否能夠正常搜索且列表顯示正常 | 支持 | 待完善 |
本地加載耗時是否保持1s內 | 支持 |
page.$()
或page.?()
方法,經嘗試選擇器僅支持組件名和類名。沒法直接獲取自定義組件內部組件元素,須要在類名前增長前綴。實際項目的頁面中大量使用自定義組件,對於自定義組件內部的結構判斷很是不方便,只能經過wxml()
方法將自定義組件內部結構打印出來才能確認內部的子組件的實際狀況。且沒法調用自定義組件內部的方法。<web-view>
組件獲取不到任何內部信息,也沒法自動化控制。但願這些問題後續可以獲得解決~~