瀏覽器領(lǐng)域,我們有如selenium和puppeteer這樣的庫(kù),可以自動(dòng)化控制瀏覽器執(zhí)行自動(dòng)化腳本,以完成自動(dòng)化端對(duì)端測(cè)試、定時(shí)自動(dòng)化任務(wù)等。隨著持續(xù)集成、持續(xù)部署也就是CI/CD的需求日益增長(zhǎng),自動(dòng)化也成為必不可少的一環(huán)。
對(duì)于日益增長(zhǎng)的小程序開(kāi)發(fā)需求,我們能不能自動(dòng)化控制小程序呢,進(jìn)而達(dá)成自動(dòng)測(cè)試、自動(dòng)發(fā)布等任務(wù)呢?
針對(duì)微信小程序,自2019年5月,微信官方也開(kāi)始提供了一個(gè)官方的自動(dòng)化SDK: miniprogram-automator 。這是一個(gè)通過(guò)NodeJS操控開(kāi)發(fā)者工具以及遠(yuǎn)程真機(jī)中微信的SDK。通過(guò)這個(gè)SDK,可以控制小程序跳轉(zhuǎn)到指定頁(yè)面、獲取小程序頁(yè)面數(shù)據(jù)、獲取小程序頁(yè)面元素狀態(tài)、觸發(fā)小程序元素綁定事件、往 AppService 注入代碼片段、調(diào)用 wx 對(duì)象上任意接口等等。
這個(gè)SDK通過(guò)腳本控制本機(jī)的微信開(kāi)發(fā)者工具來(lái)近似達(dá)到自動(dòng)化測(cè)試業(yè)務(wù)的目的,同時(shí),也可通過(guò)遠(yuǎn)程控制真機(jī),達(dá)到真機(jī)測(cè)試的目的。
我們首先來(lái)體驗(yàn)一下這個(gè)SDK。
首先,你需要確保你安裝了微信開(kāi)發(fā)者工具,并且版本大于1.02.1907232,并設(shè)置你的基礎(chǔ)庫(kù)版本在2.7.3以上,同時(shí)請(qǐng)安裝NodeJS 8.0以上的版本。
我們知道,微信開(kāi)發(fā)者工具提供了命令行與 HTTP 服務(wù)兩種接口供外部調(diào)用,開(kāi)發(fā)者可以通過(guò)命令行或 HTTP 請(qǐng)求指示工具進(jìn)行登錄、預(yù)覽、上傳等操作。
SDK通過(guò)命令行方式將微信開(kāi)發(fā)者工具調(diào)起,再通過(guò)外部方式導(dǎo)入目標(biāo)項(xiàng)目。微信開(kāi)發(fā)者工具通過(guò)讀取目標(biāo)項(xiàng)目的project.config.js,初始化項(xiàng)目。并讀取啟動(dòng)命令的 --auto-port 參數(shù),使得SDK可以通過(guò)此端口的Websocket服務(wù),實(shí)現(xiàn)與對(duì)應(yīng)的目標(biāo)小程序調(diào)試窗口進(jìn)行交互。
這就是 miniprogram-automator 工作的大致原理。
感興趣的讀者可以嘗試使用微信開(kāi)發(fā)者工具的cli方式來(lái)嘗試運(yùn)行此命令。
cli auto --project { 項(xiàng)目路徑 } --auto-port { websocket的端口 }
為了運(yùn)行上述命令,我們需要找到微信開(kāi)發(fā)者工具的安裝目錄。不同的操作系統(tǒng)位置不同。Mac位于:<安裝路徑>/Contents/MacOS/cli,windows位于: <安裝路徑>/cli.bat。對(duì)于經(jīng)常使用的讀者,建議將cli所在的目錄放在系統(tǒng)的環(huán)境變量中。
你可能遇到IDE服務(wù)超時(shí)的情況。因此,為了保證開(kāi)發(fā)者工具能夠通過(guò)命令行打開(kāi),需要將開(kāi)發(fā)者工具的HTTP服務(wù)調(diào)用接口打開(kāi)。
打開(kāi)的方式是,進(jìn)入微信開(kāi)發(fā)者工具,選擇:設(shè)置 > 安全設(shè)置。在服務(wù)端口中選擇:打開(kāi)。此時(shí),微信開(kāi)發(fā)者工具會(huì)自動(dòng)指定一個(gè)可用的端口號(hào)。
細(xì)心的讀者會(huì)發(fā)現(xiàn)這里又出現(xiàn)了一個(gè)“端口”。不同于上面提到的端口,這個(gè)端口是IDE提供對(duì)外服務(wù)的端口。如上圖所示,36146是IDE服務(wù)的端口。你在啟動(dòng)IDE之后,可以訪問(wèn) http://127.0.0.1:36146/open
你的IDE就會(huì)聚焦到你的面前。 http://127.0.0.1:36146/ 是IDE服務(wù)的根域名,open是命令,讀者可以參考 命令索引 通過(guò)不同的URL發(fā)出不同的命令。
好,安裝好了SDK,我們來(lái)操練一下:
首先,我們init一個(gè)npm庫(kù)如 auto ,通過(guò): npm i miniprogram-automator --save-dev 或 yarn add miniprogram-automator --dev 即可安裝 miniprogram-automator 。
接下來(lái),我們下載一個(gè) 微信示例程序 ,并解壓在~/demo-miniapp/
下面,我們新建一個(gè)文件,如index.js,內(nèi)容如下:
const automator = require('miniprogram-automator') automator.launch({ projectPath: '~/demo-miniapp/', // 項(xiàng)目文件地址 }).then(async miniProgram => { const page = await miniProgram.reLaunch('/page/tabBar/component/index') await page.waitFor(500) const element = await page.$('.kind-list-text') console.log(await element.attribute('class')) await element.tap() await page.waitFor(500) await miniProgram.close() })
現(xiàn)在,讓我們運(yùn)行起來(lái)。 node index.js 。我們看到,IDE自動(dòng)啟動(dòng),并加載了我們的項(xiàng)目文件,并自動(dòng)地點(diǎn)開(kāi)了第一個(gè)項(xiàng)目,過(guò)一段時(shí)間之后,程序自動(dòng)的退出。
這就是我們對(duì)于自動(dòng)運(yùn)行的初步體驗(yàn)。
截至目前(2020年4月初)最新的SDK的API主要分四個(gè)模塊:Automater、MiniProgram、Page和Element等。
Automator 模塊提供了啟動(dòng)及連接開(kāi)發(fā)者工具的方法。開(kāi)發(fā)者可以對(duì)連接地址、端口號(hào)、項(xiàng)目路徑等作出設(shè)置。歸根結(jié)底是對(duì)于cli的包裝。詳見(jiàn): Automator
MiniProgram提供對(duì)小程序的控制。提供以下幾類支持,詳見(jiàn): MiniProgram :
Page 模塊提供了控制小程序頁(yè)面的方法。提供以下幾類支持,詳見(jiàn): Page :
Element 模塊提供了控制小程序頁(yè)面元素的方法。提供以下幾類支持,詳見(jiàn): Element :
可以看出,除Automator之外,每個(gè)API模塊都在自己的領(lǐng)域內(nèi)提供對(duì)小程序自身內(nèi)容的訪問(wèn)特性以及擴(kuò)充特性。這比較類似于Pupputeer的API設(shè)定。
miniprogram-automator 本身不提供測(cè)試框架,我們可以選用熟悉的測(cè)試框架與之整合。這里我們以jest為例。其他諸如mocha、jasmine、Cucumber都比較類似。
現(xiàn)在,在我們之前的項(xiàng)目 auto 里安裝jest。 npm i jest -g 或 yarn global add jest
簡(jiǎn)單科普下jest的工作原理。在項(xiàng)目中,jest識(shí)別三種測(cè)試文件:
Jest 在進(jìn)行測(cè)試的時(shí)候,它會(huì)在整個(gè)項(xiàng)目進(jìn)行查找,只要碰到這三種文件它都會(huì)執(zhí)行。Jest有以下設(shè)定:
Jest 測(cè)試提供了一些測(cè)試的生命周期 API。可以輔助我們?cè)诿總€(gè) case 的開(kāi)始和結(jié)束做一些處理。 這樣,在進(jìn)行一些和數(shù)據(jù)相關(guān)的測(cè)試時(shí),可以在測(cè)試前準(zhǔn)備一些數(shù)據(jù),在測(cè)試后,清理測(cè)試數(shù)據(jù)。 4 個(gè)主要的生命周期函數(shù):
回到 miniprogram-automator 。我們可以在 auto 項(xiàng)目中加入一個(gè)index.spec.js文件。
const automator = require('miniprogram-automator') let miniProgram, page beforeAll(async () => { miniProgram = await automator.launch({ projectPath: '/Users/liuguanyu/devspace/demo-miniapp/' }) page = await miniProgram.reLaunch('/page/tabBar/component/index') await page.waitFor(500) }, 50000) afterAll(async () => { await miniProgram.close() })
現(xiàn)在,你在 auto 下運(yùn)行 jest 會(huì)報(bào)錯(cuò)。
意思是需要我們?cè)黾又辽僖粋€(gè)測(cè)試套件或測(cè)試用例。
為此我們?cè)黾右粋€(gè)測(cè)試套件,并增加一些測(cè)試用例,修改如下:
const automator = require('miniprogram-automator') let miniProgram, page beforeAll(async () => { miniProgram = await automator.launch({ projectPath: '/Users/liuguanyu/devspace/demo-miniapp/' }) page = await miniProgram.reLaunch('/page/tabBar/component/index') await page.waitFor(500) }, 50000) describe("測(cè)試微信小程序", () => { // 1. 測(cè)試頂部描述 it("標(biāo)題欄", async () => { const desc = await page.$('.index-desc') // 要求測(cè)試標(biāo)簽名必須為view expect(desc.tagName).toBe('view') // 要求測(cè)試內(nèi)容包含文字以下將展示小程序官方組件能力 expect(await desc.text()).toContain('以下將展示小程序官方組件能力') }) // 2. 測(cè)試列表項(xiàng) it('列表項(xiàng)', async () => { const lists = await page.$$('.kind-list-item') // 測(cè)試共有7個(gè)列表項(xiàng) expect(lists.length).toBe(7) const list = await lists[0].$('.kind-list-item-hd') //第一個(gè)列表元素的標(biāo)題應(yīng)該是“視圖窗器” expect(await list.text()).toBe('視圖容器') }) // 3. 測(cè)試列表項(xiàng)行為 it('列表行為', async () => { const listHead = await page.$('.kind-list-item-hd') // 點(diǎn)擊應(yīng)展開(kāi)未展開(kāi)項(xiàng) expect(await listHead.attribute('class')).toBe('kind-list-item-hd') await listHead.tap() await page.waitFor(200) expect(await listHead.attribute('class')).toBe( 'kind-list-item-hd kind-list-item-hd-show', ) // 再次點(diǎn)擊應(yīng)合上 await listHead.tap() await page.waitFor(200) expect(await listHead.attribute('class')).toBe('kind-list-item-hd') // 點(diǎn)擊子列表項(xiàng)應(yīng)該會(huì)跳轉(zhuǎn)到指定頁(yè)面 await listHead.tap() await page.waitFor(200) const item = await page.$('.index-bd navigator') await item.tap() await page.waitFor(1500) expect((await miniProgram.currentPage()).path).toBe('page/component/pages/view/view') }) // 4. 驗(yàn)證wxml方法和setData方法及快照比對(duì) it('驗(yàn)證WXML', async () => { const element = await page.$('page') expect(await element.wxml()).toMatchSnapshot() await page.setData({ list: [] }) expect(await element.wxml()).toMatchSnapshot() }) // 5. mock方法測(cè)試并還原 it('偽造請(qǐng)求結(jié)果', async () => { // 偽造請(qǐng)求數(shù)據(jù) const mockData = [{ rule: 'testRequest', result: { data: 'test', cookies: [], header: {}, statusCode: 200, } }] // mock方法 await miniProgram.mockWxMethod( 'request', function (obj, data) { for (let i = 0, len = data.length; i < len; i++) { const item = data[i] const rule = new RegExp(item.rule) if (rule.test(obj.url)) { return item.result } // 沒(méi)命中規(guī)則的真實(shí)訪問(wèn)后臺(tái) return new Promise(resolve => { obj.success = res => resolve(res) obj.fail = res => resolve(res) this.origin(obj) }) } }, mockData ) // 請(qǐng)求mock的方法 const result = await miniProgram.callWxMethod('request', { url: 'https://14592619.qcloud.la/testRequest', }) expect(result.data).toBe('test') // 還原方法 await miniProgram.restoreWxMethod('request') }, 30000) }) afterAll(async () => { await miniProgram.close() })
此時(shí),我們?cè)?nbsp;auto 目錄下再次運(yùn)行 jest ,則得到如下結(jié)果:
我們看到所有的測(cè)試都已通過(guò)。
真機(jī)測(cè)試可以自動(dòng)測(cè)試以及掃碼測(cè)試。此時(shí)可以在beforeAll里面加入 await miniProgram.remote(true) 。這個(gè)true如果不寫,就需要用真機(jī)掃碼測(cè)試。當(dāng)編譯好之后,開(kāi)發(fā)者工具會(huì)自動(dòng)將小程序和調(diào)試工具發(fā)送到真機(jī)。并在側(cè)邊增加了測(cè)試條。
當(dāng)不寫true時(shí)候,運(yùn)行到remote時(shí),會(huì)彈出這樣的對(duì)話框:
發(fā)布3年多,微信小程序已經(jīng)從微信生態(tài)的一環(huán),逐步向多領(lǐng)域滲透。隨著開(kāi)發(fā)者的日益增多,面向開(kāi)發(fā)的工具也逐步完善。本文試圖管中窺豹,給大家介紹了微信自動(dòng)化的主要環(huán)節(jié)。時(shí)至今日,小程序已經(jīng)成長(zhǎng)為一種重要的產(chǎn)品形式和生態(tài)環(huán)境。越來(lái)越多的小程序平臺(tái)正在自己的領(lǐng)域以不同的形式給小程序這個(gè)產(chǎn)品形式添磚加瓦。小程序生態(tài)和開(kāi)發(fā)環(huán)節(jié)的完善和成長(zhǎng),也需要廣大平臺(tái)、開(kāi)發(fā)者共同努力,將這一Created in China 的生態(tài)體系發(fā)揚(yáng)光大。