手把手從零開始小程序單元測試(附避坑指南以及源碼跟蹤)

單元測試是一個老生常談的話題,基於Web/NodeJs環境的測試框架、測試教程數不勝數,也趨於成熟了。可是對於微信小程序的單元測試,目前仍是處於起步狀態,這兩天在研究微信小程序的測試,也遇到了一些坑,在這裏記錄一下,但願給看到本文的小夥伴帶來一點幫助,少走一些彎路。node

本文內容有點多,可是乾貨滿滿,不明白的小夥伴能夠關注公衆號給我留言 git

二維碼

demo地址

github.com/xialeistudi…github

關鍵依賴版本

本文寫做時相關依賴版本以下(版本不一樣,源碼行數可能不一樣):web

  1. miniprogram-simulate: 1.0.7
  2. j-component: 1.1.6
  3. miniprogram-exparser: 0.0.6

測試流程

  1. 初始化小程序項目,編寫待測試組件
  2. 安裝jest,miniprogram-simulate測試環境
  3. 編寫測試用例
  4. 執行測試

初始化小程序項目

  1. 使用小程序開發者工具初始化新項目,APPID選擇測試號便可,語言選擇Javascript
  2. 使用小程序開發者工具新建/components/user組件
  3. components/user.js
    // components/user.js
    Component({
        data: {
            nickname: ''
        },
        methods: {
            handleUserInfo: function(e) {
                this.setData({ nickname: e.detail.userInfo.nickName })
            }
        }
    })
    複製代碼
  4. components/user.wxml
    <text class="nickname">{{nickname}}</text>
     <button class="button" open-type="getUserInfo" bindgetuserinfo="handleUserInfo">Oauth</button>
    複製代碼
  5. pages/index/index.js
    Page({
        data:{}
    })
    複製代碼
  6. pages/index/index.wxml
    <view class="container">
        <user></user>
    </view>
    複製代碼
  7. 打開小程序開發者工具,能夠看到有一個Oauth按鈕,點擊以後會在上面顯示暱稱。
  8. 由此能夠獲得測試用例點擊受權按鈕時上方顯示爲受權用戶的暱稱

安裝jest/miniprogram-simulate測試環境

  1. 因爲JS項目的小程序根目錄沒有package.json,須要手動生成一下
  2. 打開終端,在項目根目錄執行npm init -y生成package.json
  3. 安裝測試工具集npm install jest miniprogram-simulate --save-dev
  4. 編輯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"
        }
    }
    複製代碼

編寫測試用例

  1. 在項目根目錄新建tests/components/user.spec.js文件(目錄須要手動建立)
  2. 代碼以下(參考微信官方單元測試文檔編寫):
    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);
    });
    複製代碼

執行測試

  1. npm run test,等待一秒後發現,不出意外的話,測試確定過不去
  2. 部分出錯日誌:
    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)
    複製代碼
  3. 能夠推測一下緣由:
    1. dispatchEvent的事件觸發有問題,致使handleUserInfo未觸發[1]
    2. dispatchEvent的事件觸發成功,可是觸發參數有問題[2]

錯誤分析(源碼跟蹤過程)

  1. 針對第1點緣由,能夠寫一下測試代碼(components/user.js)
    Component({
         data: {
             nickname: ''
         },
         methods: {
             handleUserInfo: function(e) {
                 console.log(e);
             }
         }
     })
    複製代碼
  2. 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: {} }
    複製代碼
  3. 緣由1排除,查緣由2
  4. dispatchEvent方法是被測試組件的子組件被測試組件simulate.render函數返回
  5. 瀏覽node_modules/miniprogram-simulate/src/index.js,看到render函數(152行),能夠看到返回的組件由jComponent.create提供
  6. 瀏覽node_modules/j-component/src/index.jscreate函數,能夠看到其返回了RootComponent實例,而RootComponent是由./render/component.js提供
  7. 瀏覽node_modules/j-component/src/render/component.jsdispatchEvent函數,在這裏能夠打下日誌測試(本文就不打了,結果是這裏的options就是user.spec.js dispatchEvent函數的第二個參數detail是有值的)
  8. 繼續跟蹤源碼,因爲我們的是自定義事件,因此會走到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);
    複製代碼
  9. 能夠看到調用了exparser.Event.dispatchEvent函數,該函數的第二個參數調用了exparser.Event.create對自定義事件進行了包裝,這裏還沒到最底層,須要繼續跟蹤
  10. exparser對象是miniprogram-exparser模塊提供的,瀏覽node_modules/miniprogram-exparser/exparser.min.js,發現該文件被混淆了,不過不要緊混淆後的代碼邏輯是不變的,只不過變量名變得無心義,可讀性變差
  11. 使用webstorm格式化該文件,這裏我傳了一份格式化好的到github wxparser.js,可在線觀看
  12. 須要在源碼中搜索三個參數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;
    }
    複製代碼
  13. 能夠看到s.detail = t這個賦值,tcreate第二個參數,由node_modules/j-component/render/component.jswxparser.Event.create傳入,可是傳入的第二個參數寫死了{},因此我們的組件獲取detail的時候永遠爲{},將其修改成options.detail||{}便可,修改後代碼以下:
    exparser.Event.dispatchEvent(customEvent.target, exparser.Event.create(eventName, options.detail||{}, xxxxxx
    複製代碼
  14. 從新測試
    PASS  tests/components/user.spec.js
    複製代碼

✓ components/user (1099ms)npm

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.622s
Ran all test suites.
```
複製代碼

避坑指南

  1. querySelector用法同HTML,可是須要在組件執行,而不是組件.dom,HTML中實在DOMNode執行的
  2. dispatchEvent是觸發事件,須要在組件執行,上述代碼中是觸發button組件自定義事件
  3. dispatchEvent事件名規範: 去掉前導bind剩餘的字符串爲事件名,示例代碼中bindgetuserinfo,觸發時就是getuserinfo,若是是bindtap,那觸發時就是tap
  4. 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);
    複製代碼
  5. 因爲setTimeout的存在,觸發事件爲異步,因此寫斷言時須要加定時器

結語

小程序單元測試基本是沒什麼經驗擴借鑑,可是基於官網提供的工具,以及開源,我們遇到問題時細心排查而後修改一下,仍是能夠解決問題的。對單元測試有疑問的小夥伴能夠掃碼加我進行交流 json

微信
相關文章
相關標籤/搜索