如何使用 mocha 和 sinon 集成單元測試--單元測試示例及分析(上)

使用 mocha 集成單元測試(上)

項目地址: https://github.com/Jay-tian/j...

安裝依賴

yarn add jquery mocha  mochawesome  istanbul  sinon chai jsdom decache babel-cli babel-core babel-preset-es2015 babel-plugin-module-resolver babel-istanbul

mocha:測試框架
mochawesome:可視化報表
istanbul:覆蓋率
sinon:替換依賴
chai:斷言css

scripts 命令

命令html

"scripts": {
    "test": "mocha --timeout 5000 --recursive --reporter mochawesome --require babel-core/register tests/src && open mochawesome-report/mochawesome.html && npm run test:cover",
    "test:cover": "babel-node ./node_modules/.bin/babel-istanbul cover _mocha -- tests/src/* -R spec --recursive && open coverage/lcov-report/index.html",
    "test:s": "mocha --recursive --require babel-core/register  --timeout 5000"
  }

test 命令:執行單元測試,並打開測試報告頁面和覆蓋率頁面
test:cover 執行生成單元測試覆蓋率並打開
test:s 執行單個單元測試文件node

參數解析

--timeout 5000 超時設置
--recursive 包含子目錄
--reporter mochawesome 經過mochawesome生成報表
--require babel-core/register 經過babel轉譯es6語法
tests/src 單元測試目錄路徑
open mochawesome-report/mochawesome.html 打開頁面jquery

測試含有jQuery的代碼

初始化Jquery環境

let { JSDOM } = require('jsdom');
let dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`,{
    url: 'http://127.0.0.1',
    referrer: 'http://127.0.0.1',
    contentType: 'text/html',
    userAgent: 'Mellblomenator/9000',
    includeNodeLocations: true,
});
global.window = dom.window;
global.$ = require('jquery');

測試click事件

const { demo1 } = require('../../src/demo1.js');
const assert = require('chai').assert;
describe('demo1', function() {
  it('jquery click test', function() {
    demo1($('body'));
    assert.equal($('body').hasClass('hide'), false);
    $('body').trigger('click');
    assert.equal($('body').hasClass('hide'), true);
  });
});

運行結果

以上測試了,點擊元素時,給該元素添加一個‘hide’類的方法
模擬jquery環境和觸發click事件
圖片描述webpack

測試post事件

因爲初始化jquery環境比較通用,咱們把它放到工具類去引用git

utils.js

const decache = require('decache');
let { JSDOM } = require('jsdom');
exports.initJquery = function(html, params = {}){
    params = Object.assign({
        url: 'http://127.0.0.1',
        referrer: 'http://127.0.0.1',
        contentType: 'text/html',
        userAgent: 'Mellblomenator/9000',
        includeNodeLocations: true,
    }, params);
    let dom = new JSDOM(`<!DOCTYPE html><html><body>${html}</body></html>`, params);

    global.window = dom.window;
    decache('jquery');
    global.$ = require('jquery');
}

由於node環境中,require會有緩存,致使不一樣的單元測試間的初始環境不一致,須要手動清除緩存es6

decache('jquery');

test.demo2.js

import post from '../../src/demo2.js'; 
const utils = require('./../utils');
const sinon = require('sinon');
require('./../utils');
describe('demo2', function() {
    before(function() {
        utils.initJquery('');
    });
    
    it('jquery post', function() {
        let stubPost = sinon.stub($, 'post');
        let expectedUrl = '/demo2';
        let expectedParams = {'a': 'abc'};
        post();
        sinon.assert.calledWith(stubPost, expectedUrl, expectedParams);
        stubPost.restore();
    });
});

restore()操做 將會復原被替換的對象
mocha 有四個鉤子方法
before 在全部的單元測試運行前運行一次
after 在全部的單元測試運行結束運行一次
beforeEach 在每個的單元測試運行前運行一次
afterEach 在每個的單元測試運行後運行一次github

測試ajax

demo3.js

export default function() {
    $.ajax({
    type: 'GET',
    url: null,
    async: true,
    promise: true,
    dataType: 'json',
    beforeSend(request) {
    }
  });
}

test.demo3.js

import ajax from '../../src/demo3.js'; 
const utils = require('./../utils');
const sinon = require('sinon');
require('./../utils');
describe('demo3', function() {
    before(function() {
        utils.initJquery('');
    });
    
    it('jquery ajax', function() {
        let stubAjax = sinon.stub($, 'ajax');
        let expectedParams = {
            type: 'GET',
            url: null,
            async: true,
            promise: true,
            dataType: 'json'
        };
        ajax();
        sinon.assert.calledWithMatch(stubAjax, expectedParams);
        stubAjax.restore();
    });
});

這裏咱們使用calledWithMatch斷言參數,該方法能夠斷言傳入的參數是否正確,不須要傳入全部的參數web

測試異步代碼

demoe4.js

export default function() { 
    $('#demo4').hide();
    setTimeout(
        function(){
            $('#demo4').show();
        }, 1000);
}
import demo4 from '../../src/demo4.js'; 
const utils = require('./../utils');
const sinon = require('sinon');
const assert = require('chai').assert;
require('./../utils');
describe('asynchronous code', function() {
    let clock;
    before(function () { 
        utils.initJquery('<div id="demo4"></div>');
       
    });

    it('test by setTimeout', function(done) {
        let $demo = $('#demo4');
        demo4();
        assert.equal($demo.css('display'), 'none');
        let test = function() {
            assert.equal($demo.css('display'), 'block');
            // 這裏的done告知這個單元測試結束了,保證不影響其餘單元測試
            done();
        };

        setTimeout(test, 1001);
    });

    it('test by sinon', function() {
        //當利用了useFakeTimers後,事件將會中止
        clock = sinon.useFakeTimers(); 
        let $demo = $('#demo4');
        //運行demo4前,元素仍是顯示的
        assert.equal($demo.css('display'), 'block');
        demo4();
        //運行demo4完,元素隱藏了
        assert.equal($demo.css('display'), 'none');
        //時間穿梭101ms秒,定時器代碼還未執行,因此元素仍是隱藏的
        clock.tick(101);
        assert.equal($demo.css('display'), 'none');
        //時間再穿梭900ms秒,就到達了1001ms後,定時器代碼執行了,因此元素如今顯示了
        clock.tick(900);
        assert.equal($demo.css('display'), 'block');
        //恢復時間
        clock.restore();
    });
});

第一個單元測試利用了 setTimeout 去測試異步代碼
第二個單元測試利用了 sinon 的時空穿梭器去測試異步代碼
結果如圖所示
圖片描述ajax

第一個單元測試花了1035ms
而第二個單元測試幾乎沒有花費多少時間

因此異步代碼編寫單元測試時,第二個單元測試寫法更優

須要測試的代碼包含其餘負責業務邏輯時

demo5.js

const demo5require = require('./demo5.require.js');

export default function() { 
    if(demo5require.a() == 'a') {
        return 1;
    } else {
        return 2;
    }
}

test.demo5.js

import demo5 from '../../src/demo5.js'; 
const utils = require('./../utils');
const sinon = require('sinon');
const assert = require('chai').assert;

describe('demo5', function() {
    before(function () { 
        utils.initJquery('');
    });
    it('test', function() {
        assert.equal(demo5(), 1);
        const demo5require = require('../../src/demo5.require.js');
        let stub = sinon.stub(demo5require, 'a').returns('b');
        assert.equal(demo5(), 2);
        stub.restore();
    });
});

此時demo5依賴其餘模塊,咱們就能夠替換demo5require的方法,並指定返回值,這樣就不用關係依賴的模塊作了什麼業務。
測試結束,復原被替換的對象

webpack環境編寫單元測試

webpack中會有設置別名的狀況,這樣單元測試有可能引入的模塊的路徑有誤,這裏咱們可使用babel-plugin-module-resolver進行別名的替換

.babelrc

{
  "presets": ["es2015"],
  "plugins": [
    ["module-resolver", {
      "root": ["./"],
      "alias": {
         "common":""
      }
    }]
  ]
}

運行結果

執行命令

npm run test

如圖
圖片描述
圖片描述
圖片描述

最佳實踐總結

  • 文件名以及路徑的定義以下,這樣定義規範了路徑的書寫,便於文件的查找
/a/b/c/demo.js //待測試文件
/tests/a/b/c/test.demo.js //單元測試文件
  • 一個單元測試文件測試一個js文件
  • 一個describe測試一個方法
  • 一個it 測試一個方法中一個邏輯,這樣保證一個測試只驗證一個行爲
  • 使用sinon隔離外部調用
  • 使用before或beforeEach 初始環境
  • 使用after或afterEach 清空或還原環境,不一樣單元測試互不影響,狀態不共享
相關文章
相關標籤/搜索