本文由 IMWeb 首發於 IMWeb 社區網站 imweb.io。點擊閱讀原文查看 IMWeb 社區更多精彩文章。node
微信小程序生態日益完善,不少小程序項目頁面愈來愈多,結構愈來愈複雜,業務邏輯也更加多樣。以騰訊課堂小程序爲例,目前騰訊課堂小程序部分頁面結構和不一樣業務場景下的表現以下圖所示:程序員
能夠看到在覈心功能上主要頁面對於不一樣業務場景有衆多不一樣的表現,所以在開發與發佈的過程當中須要手動驗證大量測試用例以保證小程序按預期表現運行,善於利用工具的程序員固然會想:web
這種重複的工做能不能交給程序自動進行呢?npm
web開發中對於這類測試問題已經有了不少自動化解決方案好比Selenium、Puppeteer,思路大致相同,都是讓瀏覽器按照指定順序自動在頁面上完成點擊、輸入等操做,再將操做後的頁面表現與想要獲得的結果進行比較獲得測試結論(斷言)。那小程序中有沒有一種方案可以按照這種思路實現自動化操做並提供頁面信息用於斷言呢?爲了微信底層安全考慮,小程序環境一直比較封閉,留給開發者操做的餘地很小,自動化操做基本沒法實現,但5月底出現了miniprogram-automator(https://www.npmjs.com/package/miniprogram-automator)工具,給了小程序開發者但願。小程序
基於miniprogram-automator的文檔描述簡單總結一下,當經過命令打開開發版微信開發者工具的自動化接口並鏈接自動化接口後,此工具可提供如下能力:微信小程序
MiniProgram:獲取小程序信息(頁面堆棧、系統信息、頁面內容),控制小程序(跳轉頁面、切換tab、調用方法)瀏覽器
Page:獲取頁面信息(路徑、元素、數據、結構),控制頁面(設置渲染數據、調用方法)安全
Element:獲取元素信息(屬性、樣式、內容、位置),操控元素(點擊、長按、調用方法)微信
因此小程序自動化控制的實現依賴於開發版小程序開發者工具以及miniprogram-automator工具。小程序開發者工具命令行用來打開指定自動化操做服務端口。(開發者工具版本需高於v1.02.1906042)。miniprogram-automator工具用來操做開發者工具中運行的小程序並獲取所需的信息。對於測試需求能夠結合jest框架進行測試用例的組織和斷言。微信開發
很少廢話,看完文檔用一下:
Ø 調用開發者工具命令行打開項目與指定自動化操做服務端口
PS D:\programs\內測\微信web開發者工具> ./cli.bat --auto D:\weApp\testMiniprogram --auto-port 9420Initializing...idePortFile: C:\Users\billcui\AppData\Local\微信開發者工具\User Data\Default\.idestarting ide...IDE server has started, listening on http://127.0.0.1:35510initialization finishedOpen project with automation enabled success D:\weApp\testMiniprogram
這一行命令須要注意的有:
文檔要求開發者工具版本號必須高於v1.02.1906042,最好是最新的內測版工具,我是在v1.03.1906062運行成功的;
運行這行命令以前須要先打開開發者工具菜單中的設置->安全設置->服務端口;
自動化端口是獨立於服務端口的(好比終端打印出的35510實際上是服務端口),必需要看到Openprojectwithautomation 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中鏈接已經打開自動化端口的微信小程序項目。(這裏再也不重複代碼,見上一章)下面直接看測試內容的代碼。
1. 按鈕顯示和點擊跳轉支付頁測試
// 打開頁面,經過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()
方法來模擬點擊,這裏須要注意的是,因爲微信小程序開發者工具中點擊打開新頁面耗時較長,須要等待頁面加載一會,否則接下來獲取當前頁面路徑的時候頁面還沒跳轉過去就拿不到不到新頁面路徑了。等待的時長能夠根據經驗給個稍大的比較安全的值。
2. 點擊試學按鈕可正常播放試學視頻
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..." 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來判斷了。
3. 未購買課程時點擊課程視頻沒法播放
點擊非試看課程時,沒法播放視頻。因爲不播放視頻時頁面中只顯示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毫秒。
運行測試腳本結果以下:
目前實現的測試功能以下:
nohost檢測
首頁數據拉取、顯示、跳轉測試
付費課程詳情頁按鈕顯示、跳轉、點播、試學功能測試
優惠券按鈕顯示、領取功能測試
限時優惠按鈕顯示測試
免費課程詳情頁按鈕顯示、報名、點播功能測試
分類頁展現、跳轉列表頁、跳轉詳情頁測試
checklist中功能測試的完成狀況以下:完成度爲65%
review點 | 自動化測試 | 備註 |
---|---|---|
是否去除nohost插件 | 支持 | |
首頁是否正常顯示 | 支持 | |
pc首頁小程序登錄是否正常 | 暫不 | 信息受權沒法自動完成 |
安卓支付能力是否正常 | 暫不 | webview內部沒法獲取信息 |
分類頁是否正常顯示 | 支持 | |
是否能夠正常登錄 | 暫不 | 信息受權沒法自動完成 |
課程表是否正常展現,學習進度/直播狀態是否正常顯示 | 支持 | 待完善 |
課程詳情頁是否能夠正常展現 | 支持 | |
掃碼/分享是否正常喚起小程序 | 暫不 | 開發者工具不支持 |
付費課直播是否能夠正常播放(上雲跟騰訊視頻) | 暫不 | 開發者工具不支持直播 |
免費課直播是否能夠正常播放(上雲跟騰訊視頻) | 暫不 | 開發者工具不支持直播 |
免費課錄播是否能夠正常播放(上雲跟騰訊視頻) | 部分支持 | 開發者工具降級到tcplayer |
付費課錄播是否能夠正常播放(上雲跟騰訊視頻) | 部分支持 | 開發者工具降級到tcplayer |
試學任務是否能夠正常播放 | 支持 | |
詳情頁視頻是否正常播放 | 支持 | |
營銷工具相關顯示是否正常 | 支持 | |
是否能正常完成支付邏輯 | 暫不 | webview內部沒法獲取信息 |
類目篩選是否正常 | 支持 | 待完善 |
是否能夠正常搜索且列表顯示正常 | 支持 | 待完善 |
本地加載耗時是否保持1s內 | 支持 |
獲取頁面中的組件只能採用 page.$()
或 page.$$()
方法,經嘗試選擇器僅支持組件名和類名。沒法直接獲取自定義組件內部組件元素,須要在類名前增長前綴。實際項目的頁面中大量使用自定義組件,對於自定義組件內部的結構判斷很是不方便,只能經過 wxml()
方法將自定義組件內部結構打印出來才能確認內部的子組件的實際狀況。且沒法調用自定義組件內部的方法。
Jest的snapshot功能對於結構相對固定的組件或頁面是一種很是好的測試方式,但用起來有坑。在小程序中snapshot的對照內容一般是經過組件的wxml方法打印的字符串,但實際在運行時,wxml方法返回結果可能會不一樣,組件可能會被自動添加上wx:nodeid屬性,但有時返回字符串中又不添加,會致使snapshot測試不經過。
目前只能在開發者工具環境下測試,致使直播功能沒法測試且雲點播會自動降級爲騰訊視頻點播,直播也沒法測試。(工具更新後支持真機調試,應該有所改善)
登錄、掃碼等功能沒法測試,由於自動化控制工具沒法掃描和點擊受權彈窗。
<web-view>
組件獲取不到任何內部信息,也沒法自動化控制。
但願這些問題後續可以獲得解決~~