Mocha是JavaScript的測試框架, 瀏覽器和Node端都可以使用。可是Mocha自己並不提供斷言的功能, 須要藉助例如: Chai, 這樣的斷言庫完成測試的功能。
Mocha的API快速瀏覽, 更多細節請參考 文檔
⚠️ 注意:html
describe('unit', function () { it('example', function () { return true }) })
Mocha支持Promise, Async, callback的形式
// callback describe('異步測試Callback', function () { it('Done用例', function (done) { setTimeout(() => { done() }, 1000) }) }) // promise describe('異步測試Promise', function () { it('Promise用例', function () { return new Promise((resolve, reject) => { resolve(true) }) }) }) // async describe('異步測試Async', function () { it('Async用例', async function () { return await Promise.resolve() }) })
// before, beforeEach, 1, afterEach, beforeEach, 2, afterEach, after describe('MochaHook', function () { before(function () { console.log('before') }) after(function () { console.log('after') }) beforeEach(function () { console.log('beforeEach') }) afterEach(function () { console.log('afterEach') }) it('example1', function () { console.log(1) }) it('example2', function () { console.log(2) }) })
Mocha Hook能夠是異步的函數, 支持done,promise, async
若是beforeEach在任何descride以外添加, 那麼這個beforeEach將被視爲root hook。beforeEach將會在任何文件, 任何的測試用例前執行。
beforeEach(function () { console.log('root beforeEach') }) describe('unit1', function () { //... })
若是須要在任何測試用例前執行異步操做也能夠使用(DELAYED ROOT SUITE)。使用"mocha --delay"執行測試腳本。"mocha --delay"會添加一個特殊的函數run()到全局的上下文。當異步操做完成後, 執行run函數能夠開始執行測試用例
function deplay() { return new Promise((resolve, reject) => { setTimeout(function () { resolve() }, 1000) }) } deplay().then(function () { // 異步操做完成後, 開始執行測試 run() }) describe('unit', function () { it('example', function () { return true }) })
describe, 或者it以後添加skip。可讓Mocha忽略測試單元或者測試用例。使用skip, 測試會標記爲待處理。
設置測試失敗後, 測試重試的次數
describe('retries', function () { it('retries', function () { // 設置測試的重試次數 this.retries(3) const number = Math.random() if (number > 0.5) throw new Error() else return true }) })
對於一些接口的測試, 能夠使用動態生產測試用例, 配置請求參數的數組, 循環數組動態生成測試用例
describe('動態生成測試用例', function () { let result = [] for(let i = 0; i < 10; i ++) { result.push(Math.random()) } result.forEach((r, i) => { // 動態生成測試用例 it(`測試用例${i + 1}`, function () { return r < 1 }) }) })
若是測試用例, 運行時間超過了slow設置的時間, 會被標紅。
describe('unit', function () { it('example', function (done) { this.slow(100) setTimeout(() => { done() }, 200) }) })
設置測試用例的最大超時時間, 若是執行時間超過了最大超時時間,測試結果將爲錯誤
describe('unit', function () { it('example', function (done) { this.timeout(100) setTimeout(() => { done() }, 200) }) })
Chai是Node和瀏覽器的BDD/TDD斷言庫。下面將介紹, BDD風格的API, expect。should兼容性較差。
Chai的API快速瀏覽, 更多細節請參考 文檔
對斷言結果取反
it('not', function () { const foo = 1 expect(foo).to.equal(1) expect(foo).to.not.equal(2) })
進行斷言比較的時候, 將進行深比較
it('deep', function () { const foo = [1, 2, 3] const bar = [1, 2, 3] expect(foo).to.deep.equal(bar) expect(foo).to.not.equal(bar) })
啓用點和括號表示法
it('nested', function () { const foo = { a: [ { a: 1 } ] } expect(foo.a[0].a).to.equals(1) })
斷言時將會忽略對象prototype上的屬性
it('own', function () { Object.prototype.own = 1 const a = {} expect(a).to.not.have.own.property('own') expect(a).to.have.property('own') })
ordered.members比較數組的順序是否一致, 使用include.ordered.members能夠進行部分比較, 配合deep則能夠進行深比較
it('order', function () { const foo = [1, 2, 3] const bar = [1, 2, 3] const faz = [1, 2] const baz = [{a: 1}, {b: 2}] const fzo = [{a: 1}, {b: 2}] expect(baz).to.have.deep.ordered.members(fzo) expect(foo).to.have.ordered.members(bar) expect(foo).to.have.include.ordered.members(faz) })
要求對象至少包含一個給定的屬性
it('any', function () { const foo = {a: 1, b: 2} expect(foo).to.have.any.keys('a', 'b', 'c') })
要求對象包含所有給定的屬性
it('all', function () { const foo = {a: 1, b: 2} expect(foo).to.not.have.all.keys('a', 'c') })
用來斷言數據類型。 推薦在進行更多的斷言操做前, 首先進行類型的判斷
it('a', function () { expect('123').to.a('string') expect(false).to.a('boolean') expect({a: 1}).to.a('object') expect('123').to.an('string') expect(false).to.an('boolean') expect({a: 1}).to.an('object') })
include斷言是否包含, include能夠字符串, 數組以及對象(key: value的形式)。同時能夠配合deep進行深度比較。
it('include', function () { expect('love fangfang').to.an('string').to.have.include('fangfang') expect(['foo', 'bar']).to.an('array').to.have.include('foo', 'bar') expect({a: 1, b: 2, c: 3}).to.an('object').to.have.include({a: 1}) expect({a: {b: 1}}).to.an('object').to.deep.have.include({a: {b: 1}}) expect({a: {b: [1, 2]}}).to.an('object').to.nested.deep.have.include({'a.b[0]': 1}) })
斷言數組, 字符串長度爲空。或者對象的可枚舉的屬性數組長度爲0
進行"==="比較的斷言
能夠不使用deep進行嚴格類型的比較
範圍斷言
it('within', function () { expect(2).to.within(1, 4) })
斷言目標是否包含指定的屬性
it('property', function () { expect({a: 1}).to.have.property('a') // property能夠同時對key, value斷言 expect({a: { b: 1 }}).to.deep.have.property('a', {b: 1}) expect({a: 1}).to.have.property('a').to.an('number') })
斷言數組, 字符串的長度
it('lengthOf', function () { expect([1, 2, 3]).to.lengthOf(3) expect('test').to.lengthOf(4) })
斷言目標是否具備指定的key
it('keys', function () { expect({a: 1}).to.have.any.keys('a', 'b') expect({a: 1, b: 2}).to.have.any.keys({a: 1}) })
斷言目標是否具備指定的方法
it('respondTo', function () { class Foo { getName () { return 'fangfang' } } expect(new Foo()).to.have.respondsTo('getName') expect({a: 1, b: function () {}}).to.have.respondsTo('b') })
斷言函數的返回值爲true, expect的參數是函數的參數
it('satisfy', function () { function bar (n) { return n > 0 } expect(1).to.satisfy(bar) })
斷言目標是否爲數組的成員
it('oneOf', function () { expect(1).to.oneOf([2, 3, 1]) })
斷言函數執行完成後。函數的返回值發生變化
it('change', function () { let a = 0 function add () { a += 3 } function getA () { return a } // 斷言getA的返回值發生變化 expect(add).to.change(getA) // 斷言變化的大小 expect(add).to.change(getA).by(3) })
Karma是一個測試工具,能讓你的代碼在瀏覽器環境下測試。代碼多是設計在瀏覽器端執行的,在node環境下測試可能有些bug暴露不出來;另外,瀏覽器有兼容問題,karma提供了手段讓你的代碼自動在多個瀏覽器(chrome,firefox,ie等)環境下運行。若是你的代碼只會運行在node端,那麼你不須要用karma。vue
// 安裝karma npm install karma --save-dev
// 在package.json文件中添加測試命令 scripts: { test:unit: "karma start" }
經過Karma測試項目, 須要在項目中添加配置karam.conf.js的文件。推薦使用karam init命令生成初始化的配置文件。下面是, karam init 命令的配置項。生成配置文件以後, 就能夠經過"npm run test:unit"命令進行單元測試了。node
1. Which testing framework do you want to use(使用的測試框架)? mocha 2. Do you want to use Require.js(是否使用Require)? no 3. Do you want to capture any browsers automatically(須要測試的瀏覽器)? Chrome, IE, 4. What is the location of your source and test files(測試文件的位置)? test/*.test.js 5. Should any of the files included by the previous patterns be excluded(須要排除的文件) ? node_modules 6. Do you want Karma to watch all the files and run the tests on change(何時開始測試) ? change
// 安裝 npm install --save-dev chai karma-chai
若是測試發送在瀏覽器環境, Karma會將測試文件, 模擬運行在瀏覽器環境中。因此推薦使用webpack, babel, 對測試文件進行編譯操做。Karma中提供了處理文件中間件的配置。ps: 以前因爲瀏覽器環境不支持require, 而我在test文件中使用了require, 而且我沒有將測試文件進行編譯, 耽誤了我半天的時間:(webpack
karma.conf.js配置的更多的細節,能夠查看 karma文檔
// 安裝babel npm install --save-dev karma-webpack webpack babel-core babel-loader babel-preset-env
// 對文件添加webpack的配置, 對配置文件使用babel進行處理 module.exports = function(config) { config.set({ basePath: '', frameworks: ['mocha', 'chai'], files: [ 'test/*.test.js' ], exclude: [ 'node_modules' ], preprocessors: { 'test/*.test.js': ['webpack'] }, webpack: { // webpack4中新增的mode模式 mode: "development", module: { rules: [ { test: /\.js?$/, loader: "babel-loader", options: { presets: ["env"] }, }, ] } }, reporters: ['progress'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, concurrency: Infinity }) }
// 經過babel, 瀏覽器能夠正常的解析測試文件中的require const modeA = require('../lib/a') const expect = require('chai').expect describe('test', function () { it('example', function () { expect(modeA.a).to.equals(1) }) })
與處理瀏覽器中的require同理, 若是咱們須要對.vue文件進行測試, 則須要經過vue-loader的對.vue文件進行處理。
咱們首先經過vue-cli初始化咱們的項目, 這裏我使用的是vue-cli2.x的版本, 3.x的版本vue-cli對webpack的配置做出了抽象, 沒有將webpack的配置暴露出來, 咱們會很難理解配置。若是須要使用vue-cli3.x集成karma, 則須要另外的操做。git
// 安裝karma以及karam的相關插件 npm install karma mocha karma-mocha chai karma-chai karma-webpack --save-dev // 配置karma.conf.js // webpack的配置直接使用webpack暴露的配置 const webpackConfig = require('./build/webpack.test.conf') module.exports = function(config) { config.set({ basePath: '', frameworks: ['mocha'], files: [ 'test/*.test.js' ], exclude: [ ], // 測試文件添加中間件處理 preprocessors: { 'test/*.test.js': ['webpack'] }, webpack: webpackConfig, reporters: ['progress'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, concurrency: Infinity }) }
⚠️ 注意: 這裏依然存在一個問題Can't find variable: webpackJsonp, 咱們能夠將webpackConfig文件中的CommonsChunkPlugin插件註釋後, karma將會正常的工做。github
import { expect } from 'chai' import { mount } from '@vue/test-utils' import HelloWorld from '../src/components/HelloWorld.vue' describe('HelloWorld.vue', function () { const wrapper = mount(Counter) it('Welcome to Your Vue.js App', function () { console.log('Welcome to Your Vue.js App') expect(wrapper.vm.msg).to.equals('Welcome to Your Vue.js App') }) })
對於vue-cli3我嘗試本身添加karma.conf.js的配置, 雖然能夠運行,可是存在問題。issue中, 官方建議我在vue-cli3版本中使用vue-cli的karma的插件解決。web
對於vue-cli3,能夠使用vue-cli-plugin-unit-karma插件, 集成vue-cli3與karmavue-router
對於VueTestUtils, 我這裏並不想作過多的介紹。 由於它擁有詳盡和完善的中文文檔, 在這裏我只會作大體的概述。 文檔地址, 值得注意的一點 文檔中部份內容已通過時, 以及不適用與vue-cli3
VueTestUtils是Vue.js官方的單元測試實用工具庫, 提供不少便捷的接口, 好比掛載組件, 設置Props, 發送emit事件等操做。咱們首先使用vue-cli3建立項目, 並添加vue-cli-plugin-unit-karma的插件。而vue-cli-plugin-unit-karma插件已經集成了VueTestUtils工具, 無需重複的安裝。chrome
VueRouter, 是Vue的全局插件, 而咱們測試的都是單文件組件, 咱們該如何測試VueRouter的呢?, VueTestUtils爲咱們提供了localVue的API, 可讓咱們在測試單文件組件的時候, 使用VueRouter。(更多內容請參考文檔)
import { mount, createLocalVue } from '@vue/test-utils' import Test from '../../src/views/Test.vue' import VueRouter from 'vue-router' it('localVue Router', function () { const localVue = createLocalVue() localVue.use(VueRouter) const router = new VueRouter() // 掛載組件的同時, 同時掛載VueRouter const wrapper = mount(Test, { localVue, router }) // 咱們能夠在組件的實例中訪問$router以及$route console.log(wrapper.vm.$route) })
對於$router以及$route, 咱們也能夠經過mocks進行僞造, 並注入到組件的實例中vue-cli
it('mocks', function () { // 僞造的$route的對象 const $route = { path: '/', hash: '', params: { id: '123' }, query: { q: 'hello' } } const wrapper = mount(Test, { mocks: { $route } }) // 在組件的實例中訪問僞造的$route對象 console.log(wrapper.vm.$route) })
對於Vuex的測試, 咱們須要明確一點咱們不關心這個action或者mutation作了什麼或者這個store是什麼樣子的, 咱們只須要測試action將會在適當的時機觸發。對於getters咱們也不關心它返回的是什麼, 咱們只須要測試這些getters是否被正確的渲染。更多細節請查看文檔。
describe('Vuex', function () { // 應用全局的插件Vuex const localVue = createLocalVue() localVue.use(Vuex) let actions let store let getters let isAction = false // 在每一個測試用例執行前, 僞造action以及getters // 每一個測試用例執行前, 都會重置這些數據 beforeEach(function () { // 是否執行了action isAction = false actions = { actionClick: function () { isAction = true }, actionInput: function () { isAction = true } } getters = { name: () => '方方' } // 生成僞造的store store = new Vuex.Store({ state: {}, actions, getters }) }) // 測試是否觸發了actions it('若是text等於actionClick, 觸發了actionClick action', function () { const wrapper = mount(TestVuex, { store, localVue }) wrapper.vm.text = 'actionClick' // 若是成功觸發actionClick action, isAction將爲true expect(isAction).to.true }) it('若是text等於actionInput,觸發actionInput action', function () { const wrapper = mount(TestVuex, { store, localVue }) wrapper.vm.text = 'actionInput' // 若是成功觸發actionInput action, isAction將爲true expect(isAction).to.true }) // 對於getters, 同理action // 咱們只關注是否正確渲染了getters,並不關心渲染了什麼 it('測試getters', function () { const wrapper = mount(TestVuex, { store, localVue }) // 測試組件中使用了getters的dom, 是否被正確的渲染 expect(wrapper.find('p').text()).to.equals(getters.name()) }) })