很長一段時間以來,單元測試並非前端工程師應具有的一項技能,但隨着前端工程化的發展,項目日漸複雜化及代碼追求高複用性等,促使單元測試愈發重要,決定整個項目質量的關鍵因素之一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對象
test('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()看一下實際的渲染結果
複製代碼