項目地址: 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
命令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
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');
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
因爲初始化jquery環境比較通用,咱們把它放到工具類去引用git
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');
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
export default function() { $.ajax({ type: 'GET', url: null, async: true, promise: true, dataType: 'json', beforeSend(request) { } }); }
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
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
而第二個單元測試幾乎沒有花費多少時間
因此異步代碼編寫單元測試時,第二個單元測試寫法更優
const demo5require = require('./demo5.require.js'); export default function() { if(demo5require.a() == 'a') { return 1; } else { return 2; } }
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中會有設置別名的狀況,這樣單元測試有可能引入的模塊的路徑有誤,這裏咱們可使用babel-plugin-module-resolver進行別名的替換
{ "presets": ["es2015"], "plugins": [ ["module-resolver", { "root": ["./"], "alias": { "common":"" } }] ] }
執行命令
npm run test
如圖
/a/b/c/demo.js //待測試文件 /tests/a/b/c/test.demo.js //單元測試文件