1、起步html
1. jestvue
Jest是 Facebook 的一套開源的 JavaScript 測試框架, 它自動集成了斷言、JSDom、覆蓋率報告等開發者所須要的全部測試工具,配置較少,對vue框架友好。node
2. vue-test-utilswebpack
Vue Test Utils 是 Vue.js 官方的單元測試實用工具庫,爲jest和vue提供了一個橋樑,暴露出一些接口,讓咱們更加方便的經過Jest爲Vue應用編寫單元測試。web
3. 安裝vuex
若是已經安裝配置了webpack、babel的vue腳手架,如今須要在安裝的是:npm
npm i jest @vue/test-utils vue-jest babel-jest jest-serializer-vue -D
4. 在package.json中定義單元測試的腳本和配置jestjson
"scripts": { "test": "jest" },
"jest": { "moduleFileExtensions": [ "js", // 告訴 Jest 處理 `*.vue` 文件 "vue" ], "moduleNameMapper": { // 支持源代碼中相同的 `@` -> `src` 別名 "^@/(.*)$": "<rootDir>/src/$1" }, "transform": { // 用 `babel-jest` 處理 `*.js` 文件 "^.+\\.js$": "<rootDir>/node_modules/babel-jest", // 用 `vue-jest` 處理 `*.vue` 文件 ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest" }, "snapshotSerializers": [ // 快照的序列化工具 "<rootDir>/node_modules/jest-serializer-vue" ], //成多種格式的測試覆蓋率報告 可選 "collectCoverage": true, "collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"] }
5. 配置.babelrc文件數組
{ "presets": [ ["env", { "modules": false }] ], "env": { "test": { "presets": [ ["env"] ] } } }
6. 寫測試文件,放在根目錄下test文件夾中,以.spec.js或.test.js爲後綴名的文件,正常名字和組件名一致,如測試header組件,應該名字叫作header.spec.js或header.test.jspromise
header.vue
<template> <div> <div> <span class="count">{{ count }}</span> <button @click="increment">Increment</button> </div> </div> </template> <script> export default { data () { return { count: 0 } }, methods: { increment () { this.count++ } } } </script>
header.spec.js
import { mount } from '@vue/test-utils' import header from '@/components/header.vue' describe('header', () => { const wrapper = mount(header) it('點擊按鈕', () => { const button = wrapper.find('button'); expect(button.text()).toBe('Increment'); button.trigger('click'); expect(wrapper.find('.count').text()).toBe('1') }) })
7. 執行命令行:npm test,運行單元測試
2、jest經常使用API
1. 全局函數
1)beforeAll(fn, timeout)/afterAll(fn, timeout)
在全部測試運行以前/以後執行,第二個參數可選,默認爲5秒
2)beforeEach(fn, timeout)/afterEach(fn, timeout)
在每一個測試運行以前/以後執行,第二個參數可選,默認爲5秒
3)describe(name, fn)
建立一個塊,將幾個相關的測試組合在一塊兒。
describe.only(name, fn)只運行一次
4)test(name, fn, timeout)
測試方法,test(name, fn, timeout) 等價於 it(name, fn, timeout), 第三個參數可選,默認爲5秒
test.only(name, fn, timeout),只運行一次
2. 匹配器
1)相等、不相等匹配
toBe
test('3+3等於6', () => { expect(3 + 3).toBe(6); });
expect(3+3)返回咱們指望的結果,toBe是匹配器,匹配具體的某一個值
toEqual
若是是匹配對象,須要使用toEqual
const obj = { one: 1, two: 2 }; expect(obj).toBe({ one: 1, two: 2 });
not:匹配不相等
expect(3+3).not.toBe(6);
2)匹配真假
toBeNull 只匹配 null
toBeUndefined 只匹配 undefined
toBeDefined 與...相反 toBeUndefined
toBeTruthy匹配if聲明視爲真的任何內容
toBeFalsy匹配if語句視爲false的任何內容
3)匹配數字
toBeGreaterThan(3) 大於3
toBeGreaterThanOrEqual(3) 等於或大於3
toBeLessThan(3) 小於3
toBeLessThanOrEqual(3) 等於或小於3
對於浮點相等,請使用toBeCloseTo
const value = 0.1 + 0.2; // expect(value).toBe(0.3); //報錯 Received: 0.30000000000000004 expect(value).toBeCloseTo(0.3);
4)匹配字符串,toMatch 支持正則
test('but there is a "stop" in Christoph', () => { expect('Christoph').toMatch(/stop/); });
5)匹配數組,toContain,數組中是否包含
const shoppingList = [ 'paper towels', 'beer', ]; test('the shopping list has beer on it', () => { expect(shoppingList).toContain('beer'); });
3. 異步函數
1)回調函數
回調是異步比較常見,實現以下
function fetchData (callback) { setTimeout(() => { callback('peanut butter'); }, 1000) } test('the data is peanut butter', done => { function callback (data) { expect(data).toBe('peanut butter'); done(); } fetchData(callback); });
若是不寫done(),將會報超時的錯誤
2)promise
function fetchData () { return new Promise((resolve, reject) => { setTimeout(() => { resolve('peanut butter') }, 1000) }) } test('the data is peanut butter', () => { expect.assertions(1); return fetchData().then(data => { expect(data).toBe('peanut butter'); }); });
assertions(1)表明的是在當前的測試中至少有一個斷言是被調用的,不然斷定爲失敗。
若是刪掉return語句,那麼你的測試將在fetchData完成以前結束。
配合 .resolves/.rejects使用
test('the data is peanut butter', () => { expect.assertions(1); return expect(fetchData()).resolves.toBe('peanut butter'); });
3)async/await
使用async/await可使代碼看起來更加整潔
test('the data is peanut butter', async () => { expect.assertions(1); await expect(fetchData()).resolves.toBe('peanut butter'); });
4. mock
在項目中,一個模塊的方法內經常會去調用另一個模塊的方法。在單元測試中,咱們可能並不須要關心內部調用的方法的執行過程和結果,只想知道它是否被正確調用便可,甚至會指定該函數的返回值。此時,使用Mock函數是十分有必要。
經常使用的方法有:jest.fn()/jest.mock()/jest.spyOn
1) jest.fn()
jest.fn()是建立Mock函數最簡單的方式,若是沒有定義函數內部的實現,jest.fn()會返回undefined做爲返回值。
test('測試jest.fn()調用', () => { let mockFn = jest.fn(); let result = mockFn(1, 2, 3); // mockFn被調用 expect(mockFn).toBeCalled(); // mockFn被調用了一次 expect(mockFn).toBeCalledTimes(1); // mockFn傳入的參數爲1, 2, 3 expect(mockFn).toHaveBeenCalledWith(1, 2, 3); }) test('jest.fn()添加返回值', () => { let mockFn = jest.fn().mockReturnValue('default'); // mockFn執行後返回值爲default expect(mockFn()).toBe('default'); })
模擬另外一個模塊的調用狀況
fetch.js
export default { fetch (callback) { callback() } }
fetch.test.js
import fetch from './fetch'; test('測試jest.fn()調用', () => { let mockFn = jest.fn(); fetch.fetch(mockFn); expect(mockFn).toBeCalled() })
2)jest.mock()
fetch.js文件夾中封裝的請求方法可能咱們在其餘模塊被調用的時候,並不須要進行實際的請求。這時使用jest.mock()去mock整個模塊是十分有必要的
建立event.js文件,用來調用fetch.js
import fetch from './fetch';
export default { getPostList () { return fetch.fetch(data => { console.log('fetchPostsList be called!'); }); } }
fetch.test.js
import events from './event'; import fetch from './fetch';
jest.mock('./fetch.js'); test('mock 整個 fetch.js模塊', () => { expect.assertions(1); events.getPostList(); expect(fetch.fetch).toHaveBeenCalled(); });
使用jest.mock('./fetch.js')模擬fetch模塊,若不用jest.mock,會報錯:jest.fn() value must be a mock function or spy.
3)jest.sypOn()
在上面的jest.mock()中,並無執行console.log('fetchPostsList be called!'),說明fetch函數沒有執行。jest.spyOn()方法一樣建立一個mock函數,可是該mock函數不只可以捕獲函數的調用狀況,還能夠正常的執行被spy的函數。
import events from './event'; import fetch from './fetch'; test('使用jest.spyOn()監控fetch.fetch被正常調用', () => { expect.assertions(1); const spyFn = jest.spyOn(fetch, 'fetch'); events.getPostList(); expect(spyFn).toHaveBeenCalled(); })
使用jest.spyOn,執行了console.log('fetchPostsList be called!')。
3、vue-test-utils經常使用API
1. mount
建立一個包含被掛載和渲染的 Vue 組件的 Wrapper
import { mount } from '@vue/test-utils' import Foo from './Foo.vue' describe('Foo', () => { it('renders a div', () => { const wrapper = mount(Foo) expect(wrapper.contains('div')).toBe(true) }) })
2.shallowMount
和 mount 同樣,建立一個包含被掛載和渲染的 Vue 組件的 Wrapper,不一樣的是被存根的子組件
import { shallowMount } from '@vue/test-utils' import Foo from './Foo.vue' describe('Foo', () => { it('返回一個 div', () => { const wrapper = shallowMount(Foo) expect(wrapper.contains('div')).toBe(true) }) })
3.render render
在底層使用 vue-server-renderer 將一個組件渲染爲靜態的 HTML。
import { render } from '@vue/server-test-utils' import Foo from './Foo.vue' describe('Foo', () => { it('renders a div', async () => { const wrapper = await render(Foo) expect(wrapper.text()).toContain('<div></div>') }) })
4.createLocalVue
createLocalVue 返回一個 Vue 的類供你添加組件、混入和安裝插件而不會污染全局的 Vue 類。
可經過 options.localVue 來使用
import { createLocalVue, shallowMount } from '@vue/test-utils' import Foo from './Foo.vue' const localVue = createLocalVue() const wrapper = shallowMount(Foo, { localVue, mocks: { foo: true } }) expect(wrapper.vm.foo).toBe(true) const freshWrapper = shallowMount(Foo) expect(freshWrapper.vm.foo).toBe(false)
5.選擇器
不少方法的參數中都包含選擇器。一個選擇器能夠是一個 CSS 選擇器、一個 Vue 組件或是一個查找選項對象。
標籤選擇器 (div、foo、bar)
類選擇器 (.foo、.bar)
特性選擇器 ([foo]、[foo="bar"])
id 選擇器 (#foo、#bar)
僞選擇器 (div:first-of-type)
近鄰兄弟選擇器 (div + .foo)
通常兄弟選擇器 (div ~ .foo)
const buttonr = wrapper.find('.button') const content = wrapper.find('#content')
6.僞造 $route 和 $router
有的時候你想要測試一個組件在配合 $route 和 $router 對象的參數時的行爲。這時候你能夠傳遞自定義假數據給 Vue 實例。
import { shallowMount } from '@vue/test-utils' const $route = { path: '/home' } const wrapper = shallowMount(Component, { mocks: { $route } }) expect(wrapper.vm.$route.path).toBe('/home')
7.測試vuex
在測試vuex,主要經過僞造state/getters/mutations/actions進行測試
.vue文件中
<template> <div> <h1>{{appName}}</h1> <h2>{{appNameWithVersion}}</h2> <button @click="updateAppName" class="mutation">mutation update</button> <button @click="updateActionAppName" class='action'>action update</button> </div> </template> <script> export default { methods: { updateAppName () { this.$store.commit('setAppName', 'admin2') }, updateActionAppName () { this.$store.dispatch('setName', 'admin3') } }, computed: { appName () { return this.$store.state.appName; }, appNameWithVersion () { return this.$store.getters.appNameWithVersion; } } } </script>
.spec.js文件中
import { shallowMount, createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' import Header from '@/components/Header.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('header.vue', () => { let wrapper; let store, state, getters, mutations, actions; beforeEach(() => { state = { appName: 'admin' }; getters = { appNameWithVersion: () => 'getters' } mutations = { setAppName: jest.fn(), } actions = { setName: jest.fn(), } store = new Vuex.Store({ state, getters, mutations, actions }) wrapper = shallowMount(Header, { store, localVue }) }) it('測試vuex', () => { expect(wrapper.find('h1').text()).toBe(state.appName) expect(wrapper.find('h2').text()).toBe(getters.appNameWithVersion()) wrapper.find('.mutation').trigger('click') expect(mutations.setAppName).toHaveBeenCalled() wrapper.find('.action').trigger('click') expect(actions.setName).toHaveBeenCalled() }) })
完...
參考:
1.https://vue-test-utils.vuejs.org/zh/
2.https://jestjs.io/docs/en/getting-started
3.http://www.cnblogs.com/Wolfmanlq/p/8018370.html
4.https://www.jianshu.com/p/70a4f026a0f1
5.https://www.jianshu.com/p/ad87eaf54622