很長一段時間以來,單元測試並非前端工程師應具有的一項技能,但隨着前端工程化的發展,項目日漸複雜化及代碼追求高複用性等,促使單元測試愈發重要,決定整個項目質量的關鍵因素之一html
簡單來講就是TDD先寫測試模塊,再寫主功能代碼,而後能讓測試模塊經過測試,而BDD是先寫主功能模塊,再寫測試模塊前端
斷言指的是一些布爾表達式,在程序中的某個特定點該表達式值爲真,判斷代碼的實際執行結果與預期結果是否一致,而斷言庫則是講經常使用的方法封裝起來vue
主流的斷言庫有node
assert("mike" == user.name); 複製代碼
expect(foo).to.be("aa"); 複製代碼
foo.should.be("aa"); //should 複製代碼
Jest 是 Facebook 開源的一款 JS 單元測試框架,它也是 React 目前使用的單元測試框架,目前vue官方也把它看成爲單元測試框架官方推薦 。 目前除了 Facebook 外,Twitter、Airbnb 也在使用 Jest。Jest 除了基本的斷言和 Mock 功能外,還有快照測試、實時監控模式、覆蓋度報告等實用功能。 同時 Jest 幾乎不須要作任何配置即可使用。ios
我在項目開發使用jest做爲單元測試框架,結合vue官方的測試工具vue-util-testchrome
npm install --save-dev jest
npm install -g jest
複製代碼
(1)添加方式vue-cli
npx jest --init
複製代碼
而後會有一些選擇,根據本身的實際狀況選擇npm
回車後會在項目目錄下自動生成 Jest.config.js配置文件,固然也能夠選擇第二種,手動建立const path = require('path'); module.exports = { verbose: true, rootDir: path.resolve(__dirname, '../../../'), moduleFileExtensions: [ 'js', 'json', 'vue', ], testMatch: [ '<rootDir>/src/test/unit/specs/*.spec.js', ], transform: { '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest', }, moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, transformIgnorePatterns: ['/node_modules/'], collectCoverage: false, coverageReporters: ['json', 'html'], coverageDirectory: '<rootDir>/src/test/unit/coverage', collectCoverageFrom: [ 'src/components/**/*.(js|vue)', '!src/main.js', '!src/router/index.js', '!**/node_modules/**', ], }; 複製代碼
配置解析:json
vue-jest
處理 *.vue
文件,用babel-jest
處理 *.js
文件@
-> src
別名(2)jest命令行工具axios
{ "name": "test", "version": "1.0.0", "scripts": { "unit": "jest --config src/test/unit/jest.conf.js --coverage", }, dependencies": { "vue-jest": "^3.0.5", }, "devDependencies":{ "@vue/test-utils": "^1.0.0-beta.13", "babel-core": "^7.0.0-bridge.0", "babel-jest": "^21.2.0", "jest": "^21.2.1", } } 複製代碼
config - 配置jest配置文件路徑
coverage - 生成測試覆蓋率報告
coverage是jest提供的生成測試覆蓋率報告的命令,須要生成覆蓋率報告的在package.json添加--coverage參數
(3) 單元測試文件命名
以spec.js結尾命名,spec是sepcification的縮寫。就測試而言,Specification指的是給定特性或者必須知足的應用的技術細節
(4)單元測試報告覆蓋率指標
執行: npm run unit
配置後執行該命令會直接生成coverage文件並在終端顯示各個指標的覆蓋率概覽
在網頁中打開coverage目錄下的index.html就能夠看到具體每一個組件的測試報告
當咱們完成單元測試覆蓋率達不到100%,不用慌,不用過分追求100%的覆蓋率,把核心的功能模塊測通便可,固然若是你要設置最低的覆蓋率檢測,能夠在配置中加入以下,若是覆蓋率低於你所設置的閾值(80%),則測試結果失敗不經過
//jest.config.js coverageThreshold: { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } }, 複製代碼
🚀官方文檔
expect(1+1).toBe(2)//判斷兩個值是否相等,toBe不能判斷對象,須要判斷對象要使用toEqual expect({a: 1}).toEqual({a: 1});//會遞歸檢查對象的每一個字段 expect(1).not.toBe(2)//判斷不等 expect(n).toBeNull(); //判斷是否爲null expect(n).toBeTruthy(); //判斷結果爲true expect(n).toBeFalsy(); //判斷結果爲false expect(value).toBeCloseTo(0.3); // 浮點數判斷相等 expect(compileAndroidCode).toThrow(ConfigError); //判斷拋出異常 複製代碼
Vue Test Utils 是 Vue.js 官方的單元測試實用工具庫,經過二者結合來測試驗證碼組件,覆蓋各功能測試
//kAuthCode <template> <p class="kauthcode"> <span class="kauthcode_btn" v-if="vertification" @click="handleCode"> 獲取驗證碼</span> <span v-else>{{timer}} 秒從新獲取</span> </p> </template> <script> export default { name: 'KAuthCode', props: { phone: { type: String, require: true, }, type: { type: String, default: '1', require: true, validator(t) { return ['1', '2'].includes(t);// 1手機 2郵箱 }, }, validateType: { type: String, default: '1', validator(t) { return ['1', '2', '3'].includes(t);// 1 消息 2 表單 3自定義 }, }, }, data() { return { timer: 60, vertification: true, }; }, methods: { handleCode() { if (!this.phone) { switch (this.type) { case '1': this.$Message.warning('手機號碼不能爲空'); break; case '2': this.$refs.formRef.validateField('code'); break; default: break; } return; } this.getCode(); }, getCode() { let response; switch (this.type) { case '1': response = this.$api.login.getPhoneCode({ mobileNumber: this.phone }); break; case '2': response = this.$api.login.getEmailCode({ email: this.phone }); break; default: break; } response.then(() => { this.$Message.success('驗證碼發送成功'); this.vertification = false; const codeTimer = setInterval(() => { this.timer -= 1; if (this.timer <= 0) { this.vertification = true; this.timer = 60; clearInterval(codeTimer); } }, 1000); }); }, }, }; </script> <style lang="less" scoped> .kauthcode { span { display: inline-block; width: 100%; } } </style> 複製代碼
測試文件
// kAuthCode.spec.js import {createLocalVue, mount, shallowMount} from '@vue/test-utils'; import KAuthCode from '@/components/common/KAuthCode.vue'; import login from '@/service/modules/login.js'; import iviewUI from 'view-design'; const localVue = createLocalVue(); localVue.use(iviewUI); const testPhone = '18898538706'; jest.mock('@/service/modules/login.js', () => ({ getPhoneCode: () => Promise.resolve({ data: { answer: 'mock_yes', image: 'mock.png', } }) })) describe('KAuthCode.vue', () => { const option = { propsData: { // phone: testPhone, type: '2' }, mocks: { $api: { login }, }, }; beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.clearAllTimers(); }); const wrapper = mount(KAuthCode, option); it('設置手機號碼', () => { const getCode = jest.fn(); option.methods = {getCode}; wrapper.find('.kauthcode_btn').trigger('click'); expect(wrapper.vm.phone).toBe(testPhone); }); it('沒有設置手機號碼應報錯', () => { wrapper.setData({type:'2'}); const status = wrapper.find('.kauthcode_btn').trigger('click'); expect(status).toBeFalsy(); }); }); 複製代碼
一個 Wrapper 是一個包括了一個掛載組件或 vnode,以及測試該組件或 vnode 的方法, 經過用mount(component,option)來掛載組件,獲得wrapper包裹器,可經過
wrapper.vm
訪問實際的 Vue 實例wrapper.setData
修改實例wrapper.find
找到相應的dom並觸發事件`wrapper.find('.kauthcode_btn').trigger('click');propsData
- 組件被掛載時對props的設置import {createLocalVue, mount, shallowMount} from '@vue/test-utils'; import KAuthCode from '@/components/common/KAuthCode.vue'; const option = { propsData: { // phone: testPhone, type: '2' }, mocks: { $api: { login }, }, }; const wrapper = mount(KAuthCode, option); 複製代碼
ps: 也能夠經過shallowMount來掛載組件,區別在於shallowMount不會渲染子組件,詳細區別,能夠經過shallowMount和mount兩個方法分別掛載同組件並進行快照測試後查看所生成文件內容
返回一個 Vue 的類供你添加組件、混入和安裝插件而不會污染全局的 Vue 類
import {createLocalVue, mount} from '@vue/test-utils'; import iviewUI from 'view-design'; const localVue = createLocalVue(); localVue.use(iviewUI); 複製代碼
beforeEach和afterEach - 在同一個describe描述中,beforeAll和afterAll會在多個it做用域內執行,適合作一次性設置
調用順序: beforeAll => beforeEach => afterAll => afterEach
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
});
複製代碼
三個與 Mock 函數相關的API,分別是jest.fn()、jest.spyOn()、jest.mock()
// 斷言mockFn執行後返回值爲name it('jest.fn()返回值', () => { let mockFn = jest.fn().mockReturnValue('name'); expect(mockFn()).toBe('name'); }) //定義jest.fn()的內部實現並斷言其結果 it('jest.fn()的內部實現', () => { let mockFn = jest.fn((a, b) => { return a + b; }) expect(mockFn(2, 2)).toBe(4); }) //jest.fn()返回Promise對象 it('jest.fn()返回Promise', async () => { let mockFn = jest.fn().mockResolvedValue('name'); let result = await mockFn(); // 斷言mockFn經過await關鍵字執行後返回值爲name expect(result).toBe('name'); // 斷言mockFn調用後返回的是Promise對象 expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]"); }) 複製代碼
// kAuthCode.spec.js jest.mock('@/service/modules/login.js', () => ({ getPhoneCode: () => Promise.resolve({ data: { answer: 'mock_yes', image: 'mock.png', } }) })) it('設置手機號碼', () => { const getCode = jest.fn(); option.methods = {getCode}; wrapper.find('.kauthcode_btn').trigger('click'); expect(getCode).toHaveBeenCalled() expect(wrapper.vm.phone).toBe(testPhone); }); 複製代碼
須要用mock掉整個axios的請求,使用toHaveBeenCalled判斷這個方法是否被調用就能夠了
這個例子裏面,咱們只需關注getCode方法,其餘能夠忽略。爲了測試這個方法,咱們應該作到:
注:有時候會存在一種狀況,在同個組件中調用同個方法,只是返回值不一樣,咱們可能要對它進行屢次不一樣的mock,這時候須要在beforeEach使用restoreAllMocks方法重置狀態
mock的目的:
1.觸發事件 - 假設組件庫使用的是iview中對<Checkbox>提供的@change事件,可是當咱們進行 wrapper.trigger('change')時,是觸發不了的。<Button>的@click()和<button>的@click也是有區別的。 2。渲染問題 - 組件庫提供的組件渲染後的html,須要經過wrapper.html()來看,可能會與你從控 制臺看到的html有所區別,爲避免測試結果出錯,還應console.log一下wrapper.html()看一下實際的渲染結果 複製代碼