單元測試是一個老生常談的話題,基於Web/NodeJs環境的測試框架、測試教程數不勝數,也趨於成熟了。可是對於微信小程序的單元測試,目前仍是處於起步狀態,這兩天在研究微信小程序的測試,也遇到了一些坑,在這裏記錄一下,但願給看到本文的小夥伴帶來一點幫助,少走一些彎路。node
本文內容有點多,可是乾貨滿滿,不明白的小夥伴能夠關注公衆號給我留言git
https://github.com/xialeistudio/miniprogram-unit-test-demogithub
本文寫做時相關依賴版本以下(版本不一樣,源碼行數可能不一樣):web
測試號
便可,語言選擇Javascript
。/components/user
組件components/user.js
npm
// components/user.js Component({ data: { nickname: '' }, methods: { handleUserInfo: function(e) { this.setData({ nickname: e.detail.userInfo.nickName }) } } })
components/user.wxml
json
<text class="nickname">{{nickname}}</text> <button class="button" open-type="getUserInfo" bindgetuserinfo="handleUserInfo">Oauth</button>
pages/index/index.js
小程序
Page({ data:{} })
pages/index/index.wxml
微信小程序
<view class="container"> <user></user> </view>
Oauth
按鈕,點擊以後會在上面顯示暱稱。點擊受權按鈕時上方顯示爲受權用戶的暱稱
package.json
,須要手動生成一下npm init -y
生成package.json
npm install jest miniprogram-simulate --save-dev
編輯package.json
,在scripts
新建test
命令微信
{ "name": "unit-test-demo", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "test": "jest" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { "jest": "^24.8.0", "miniprogram-simulate": "^1.0.7" } }
tests/components/user.spec.js
文件(目錄須要手動建立)代碼以下(參考微信官方單元測試文檔編寫):app
const simulate = require('miniprogram-simulate'); const path = require('path'); test('components/user', (done) => { // 定義測試名稱,傳入done表示當前測試是異步測試,須要回調函數來告訴jest,我測試執行完畢 const id = simulate.load(path.join(__dirname, '../../components/user')); // 加載組件 const component = simulate.render(id); // 渲染組件 const text = component.querySelector('.nickname'); // 獲取nickname節點 const button = component.querySelector('.button'); // 獲取button節點 button.dispatchEvent('getuserinfo', { // 模擬觸發事件 detail: { // 傳遞事件參數 userInfo: { nickName: 'hello', }, }, }); setTimeout(() => { // 異步斷言 expect(text.dom.innerHTML).toBe('hello'); // 檢測text節點的innerHTML等於模擬受權獲取的暱稱 done(); }, 1000); });
npm run test
,等待一秒後發現,不出意外的話,測試確定過不去
部分出錯日誌:
Expected: "hello" Received: "" at toBe (/Users/xialeistudio/WeChatProjects/unit-test-demo/tests/components/user.spec.js:18:32) at Timeout.callback [as _onTimeout] (/Users/xialeistudio/WeChatProjects/unit-test-demo/node_modules/jsdom/lib/jsdom/browser/Window.js:678:19) at listOnTimeout (internal/timers.js:535:17) at processTimers (internal/timers.js:479:7)
能夠推測一下緣由:
針對第1點緣由,能夠寫一下測試代碼(components/user.js
)
Component({ data: { nickname: '' }, methods: { handleUserInfo: function(e) { console.log(e); } } })
npm run test
,能夠看到事件仍是成功觸發了,不過detail
是{}
console.log components/user.js:21 { type: 'getuserinfo', timeStamp: 948, target: { id: '', offsetLeft: 0, offsetTop: 0, dataset: {} }, currentTarget: { id: '', offsetLeft: 0, offsetTop: 0, dataset: {} }, detail: {}, touches: {}, changedTouches: {} }
dispatchEvent
方法是被測試組件的子組件
,被測試組件
由simulate.render
函數返回node_modules/miniprogram-simulate/src/index.js
,看到render函數(152行)
,能夠看到返回的組件由jComponent.create
提供node_modules/j-component/src/index.js
的create
函數,能夠看到其返回了RootComponent
實例,而RootComponent
是由./render/component.js
提供node_modules/j-component/src/render/component.js
的dispatchEvent
函數,在這裏能夠打下日誌測試(本文就不打了,結果是這裏的options就是user.spec.js
dispatchEvent
函數的第二個參數
,detail
是有值的)繼續跟蹤源碼,因爲我們的是自定義事件
,因此會走到91行
的代碼,該代碼塊以下:
// 自定義事件 const customEvent = new CustomEvent(eventName, options); // 模擬異步狀況 setTimeout(() => { dom.dispatchEvent(customEvent); exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, {}, { originalEvent: customEvent, bubbles: true, capturePhase: true, composed: true, extraFields: { touches: options.touches || {}, changedTouches: options.changedTouches || {}, }, })); }, 0);
exparser.Event.dispatchEvent
函數,該函數的第二個參數
調用了exparser.Event.create
對自定義事件進行了包裝,這裏還沒到最底層,須要繼續跟蹤exparser
對象是miniprogram-exparser模塊
提供的,瀏覽node_modules/miniprogram-exparser/exparser.min.js
,發現該文件被混淆了,不過不要緊混淆後的代碼邏輯是不變的,只不過變量名變得無心義,可讀性變差
須要在源碼中搜索三個參數
的create
函數(Object.create不算
),須要有耐心,通過排查後發現168行代碼應該是目標代碼
i.create = function(e, t, r) { r = r || {}; var n = r.originalEvent, o = r.extraFields || {}, a = Date.now() - l, s = new i; s.currentTarget = null, s.type = e, s.timeStamp = a, s.mark = null, s.detail = t, s.bubbles = !!r.bubbles, s.composed = !!r.composed, s.__originalEvent = n, s.__hasCapture = !!r.capturePhase, s.__stopped = !1, s.__dispatched = !1; for (var u in o) s[u] = o[u]; return s; }
能夠看到s.detail = t
這個賦值,t
是create
的第二個參數
,由node_modules/j-component/render/component.js
的wxparser.Event.create
傳入,可是傳入的第二個參數寫死了{}
,因此我們的組件獲取detail
的時候永遠爲{}
,將其修改成options.detail||{}
便可,修改後代碼以下:
exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, options.detail||{}, xxxxxx
從新測試
PASS tests/components/user.spec.js ✓ components/user (1099ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 3.622s Ran all test suites.
querySelector
用法同HTML,可是須要在組件
執行,而不是組件.dom
,HTML中實在DOMNode
執行的dispatchEvent
是觸發事件,須要在組件
執行,上述代碼中是觸發button組件
的自定義事件
dispatchEvent
事件名規範: 去掉前導bind剩餘的字符串爲事件名
,示例代碼中bindgetuserinfo
,觸發時就是getuserinfo
,若是是bindtap
,那觸發時就是tap
dispatchEvent
底層是j-component
這個npm模塊實現
的,跟蹤源碼發現執行是異步的(代碼文件node_modules/j-component/src/render/component.js
,函數名dispatchEvent
)
// 自定義事件 const customEvent = new CustomEvent(eventName, options); // 模擬異步狀況 setTimeout(() => { dom.dispatchEvent(customEvent); exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, {}, { originalEvent: customEvent, bubbles: true, capturePhase: true, composed: true, extraFields: { touches: options.touches || {}, changedTouches: options.changedTouches || {}, }, })); }, 0);
setTimeout
的存在,觸發事件爲異步,因此寫斷言時須要加定時器小程序單元測試基本是沒什麼經驗擴借鑑,可是基於官網提供的工具,以及開源
,我們遇到問題時細心排查而後修改一下,仍是能夠解決問題的。對單元測試有疑問的小夥伴能夠掃碼加我進行交流