首先,我認爲前端測試並非全部項目都必須的,由於寫測試代碼是須要要花費必定時間的,當項目比較簡單的時候,花時間寫測試代碼可能反而會影響開發效率,可是須要指出的是,咱們前端開發過程當中,編寫測試代碼,有如下這些好處:javascript
TDD
和BDD
TDD
與單元測試TDD
所謂TDD(Test Driven Development)
,即測試驅動開發,簡單的來講就是先編寫測試代碼,而後以使得全部測試代碼都經過爲目的,編寫邏輯代碼,是一種以測試來驅動開發過程的開發模式。html
所謂單元測試(unit testing)
,是指對軟件中的最小可測試單元進行檢查和驗證。通俗的講,在前端,單元能夠理解爲一個獨立的模塊文件,單元測試就是對這樣一個模塊文件的測試。前端
對於一個獨立的模塊(ES6
模塊),由於功能相對獨立,因此咱們能夠首先編寫測試代碼,而後根據測試代碼指導編寫邏輯代碼。vue
因此提到TDD
,這裏的測試通常是指單元測試java
BDD
與集成測試BDD
所謂BDD(Behavior Driven Development)
,即行爲驅動開發,簡單的來講就是先編寫業務邏輯代碼,而後以使得全部業務邏輯按照預期結果執行爲目的,編寫測試代碼,是一種以用戶行爲來驅動開發過程的開發模式。node
所謂集成測試(Integration Testing)
,是指對軟件中的全部模塊按照設計要求進行組裝爲完整系統後,進行檢查和驗證。通俗的講,在前端,集成測試能夠理解爲對多個模塊實現的一個交互完整的交互流程進行測試。react
對於多個模塊(ES6
模塊)組成的系統,須要首先將交互行爲完善,才能按照預期行爲編寫測試代碼。webpack
因此提到BDD
,這裏的測試通常是指集成測試。ios
Jest
使用---引言部分若是咱們以前歷來沒有接觸過測試代碼,那讓咱們本身來設計測試代碼的寫法,會是什麼樣呢?咱們須要讓測試代碼簡單,通俗易懂,好比咱們舉個例子以下:git
export function findMax (arr) { return Math.max(...arr) } 複製代碼
咱們寫了一個很簡單的獲取數組最大值的函數(你可能以爲這樣寫並不嚴謹,但咱們爲了簡單,暫時假設輸入是非空數值數組),若是對這個函數寫一個測試其正確與否的測試程序,它可能構思是這樣的:
我指望 findMax([1, 2, 4, 3]) 的結果是 4 複製代碼
進一步轉化爲英文:
I expect findMax([1, 2, 4, 3]) to be 4 複製代碼
用程序性的語言表示,expect
做爲一個函數,爲它傳入想要測試的對象(findMax
函數),把輸出結果也作一層封裝toBe(4)
:
expect(findMax([1, 2, 4, 3])).toBe(4) // 有內味了 複製代碼
更進一步,咱們想要增長一些描述性信息,好比
測試findMax函數,我指望 findMax([1, 2, 4, 3]) 的結果是 4 複製代碼
這個時候,咱們能夠再作一層封裝,定義一個test
函數,它有兩個參數,第一個參數是一些描述性信息(這裏是 測試findMax
函數),第二個參數是一個函數,函數裏能夠執行咱們上面的邏輯,以下:
test('findMax函數輸出', () => { expect(findMax([1, 2, 4, 3])).toBe(4) // 內味更深了 }) 複製代碼
咱們本身能夠簡單的實現下test
函數和expect
函數,由於存在鏈式調用toBe
,因此expect
函數最終應該返回一個具備toBe
方法的對象,以下:
// expect函數 function expect (value) { return { toBe: (toBeValue) => { if (toBeValue === value) { console.log('測試經過!') } else { throw new Error('測試不經過!') } } } } // test函數 function test (msg, func) { try { func() console.log(`${msg}測試過程無異常!`) } catch (err) { console.error(`${msg}測試過程出錯!`) } } 複製代碼
咱們的測試方法,只是對數字作了簡單的測試,實際項目中,須要測試的類型是不少的,這個時候咱們就能夠選擇一些比較成熟的測試框架。一個簡單好用,功能強大的工具就呈如今咱們面前,它就是jest
。
Jest
使用---入門部分咱們這部分的例子主要是爲了介紹jest
最基本的用法,首先咱們先簡單的搭建一下演示環境。
第一步,使用npm init -y
(個人node
版本是v12.14.1
,npm
版本是v6.13.4
)初始化項目
第二步,安裝jest
npm install --save-dev jest
(安裝能夠參考官網)
第三步,運行npx jest --init
命令,生成一份jest的配置文件jest.config.js
,個人選擇以下
第四步,運行npm i babel-jest @babel/core @babel/preset-env -D
安裝babel
,而且配置.babelrc
以下
{ presets: [ [ '@babel/preset-env', { targets: { node: 'current', }, }, ], ], }; 複製代碼
第五步,根目錄下創建src
文件夾,新建兩個文件basic.js
和basic.test.js
第六步,package.json
增長一條命令:
"scripts": {
"test": "jest"
},
複製代碼
以上六步完成後,咱們的項目結構應該以下圖
jest
用法接下來咱們採用TDD
加單元測試的方式來學習jest基本用法:
首先,在basic.js
裏定義兩個工具函數
// 1. 尋找最大值 export function findMax (arr) { } // 2. 給定一個整數數組 nums 和一個目標值 target,在該數組中找出和爲目標值的那 兩個 整數,若是存在,返回true,不然返回false export function twoSum (nums, target) { }; 複製代碼
既然是TDD
,咱們首先編寫測試代碼,在這個過程當中,咱們逐步學習各類jest
的基本用法。測試代碼在basic.test.js
文件中編寫:
import { findMax, twoSum } from './basic' // 指望findMax([2, 6, 3])執行後結果爲6 test('findMax([2, 6, 3])', () => { expect(findMax([2, 6, 3])).toBe(6) }) // 指望twoSum([2, 3, 4, 6], 10)執行後結果爲true test('twoSum([2, 3, 4, 6], 10)', () => { expect(twoSum([2, 3, 4, 6], 10)).toBe(true) }) 複製代碼
從上面代碼,咱們能夠看到,jest
測試代碼的寫法,和以前咱們本身寫的是同樣的(固然啦,原本就是模仿jest
的),此時咱們運行npm test
命令,觀察命令行輸出以下:
Expected
表明指望函數執行的結果,也就是
toBe
裏的那個值,
Received
表明實際執行函數獲得的結果,由於咱們尚未編寫業務代碼,因此
Received
都是
undefined
,最後顯示一共
1
個測試文件(
Test Suites
)和
2
條測試代碼,它們都測試失敗了。
接下來咱們完善basic.js
裏的邏輯
// 1. 尋找最大值 export function findMax (arr) { return Math.max(...arr) } // 2. 給定一個整數數組 nums 和一個目標值 target,在該數組中找出和爲目標值的那 兩個 整數,若是存在,返回true,不然返回false export function twoSum (nums, target) { for (let i = 0; i < nums.length - 1; i++) { for (let j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] === target) { return true } } } return false }; 複製代碼
而後咱們再次運行npm test
,獲得結果以下
TDD
和單元測試的開發過程。
jest
matchers
像是上小節,在expect
函數後面跟着的判斷結果的toBe
在jest
中被稱爲matcher
,咱們這一小節就來介紹另一些經常使用的matchers
toEqual
咱們首先改造下剛剛的twoSum
函數,讓它返回找到的兩個數的索引數組(leetcode
第一題)
// 2. 給定一個整數數組 nums 和一個目標值 target,在該數組中找出和爲目標值的那 兩個 整數, // 並返回他們的數組下標(假設每種輸入只會對應一個答案,數組中同一個元素不能使用兩遍)。 export function twoSum (nums, target) { for (let i = 0; i < nums.length - 1; i++) { for (let j = i + 1; j < nums.length; j++) { if (nums[i] + nums[j] === target) { return [i, j] } } } return [] }; 複製代碼
接下來測試代碼部分咱們只保留對twoSum
函數的測試,並同步修改測試代碼
test('twoSum([2, 3, 4, 6], 10)', () => { expect(twoSum([2, 3, 4, 6], 10)).toBe([2, 3]) }) 複製代碼
咱們的指望是函數執行的結果是[2, 3]
這樣的數組,看起來沒問題,運行npm test
咱們發現並無經過測試,這是由於,toBe
能夠判斷基本類型數據,可是對於數組,對象這樣的引用類型是沒辦法判斷的,這個時候,咱們就須要使用toEqual
test('twoSum([2, 3, 4, 6], 10)', () => { expect(twoSum([2, 3, 4, 6], 10)).toEqual([2, 3]) }) 複製代碼
改爲toEqual
以後,測試代碼就成功了
matchers
這部份內容很簡單,也比較多,因此直接在代碼裏註釋說明:
test('變量a是否爲null', () => { const a = null expect(a).toBeNull() }) test('變量a是否爲undefined', () => { const a = undefined expect(a).toBeUndefined() }) test('變量a是否爲defined', () => { const a = null expect(a).toBeDefined() }) test('變量a是否爲true', () => { const a = 1 expect(a).toBeTruthy() }) test('變量a是否爲false', () => { const a = 0 expect(a).toBeFalsy() }) 複製代碼
測試結果以下:
not
修飾符很簡單,not
就是對matcher
的否認
test('test not', () => { const temp = 10 expect(temp).not.toBe(11) expect(temp).not.toBeFalsy() expect(temp).toBeTruthy() }) 複製代碼
測試結果以下:
matchers
這部份內容很簡單,也比較多,因此直接在代碼裏註釋說明:
// 判斷數num是否大於某個數 test('toBeGreaterThan', () => { const num = 10 expect(num).toBeGreaterThan(7) }) // 判斷數num是否大於等於某個數 test('toBeGreaterThanOrEqual', () => { const num = 10 expect(num).toBeGreaterThanOrEqual(10) }) // 判斷數num是否小於某個數 test('toBeLessThan', () => { const num = 10 expect(num).toBeLessThan(20) }) // 判斷數num是否小於等於某個數 test('toBeLessThanOrEqual', () => { const num = 10 expect(num).toBeLessThanOrEqual(10) expect(num).toBeLessThanOrEqual(20) }) 複製代碼
測試結果以下:
上面介紹的都是整數判斷,十分簡單,可是若是是浮點數相關的判斷,會不太同樣,好比,咱們知道0.1 + 0.2 = 0.3
這個式子在數學中沒有問題,可是在計算機中,因爲精度問題,這個0.1 + 0.2
結果若是用toBe
結果並非準確的0.3
,若是咱們想要判斷浮點數的相等,在jest
中提供了一個toBeCloseTo
的matcher
能夠解決:
test('toBe', () => { const sum = 0.1 + 0.2 expect(sum).toBe(0.3) }) test('toBeCloseTo', () => { const sum = 0.1 + 0.2 expect(sum).toBeCloseTo(0.3) }) 複製代碼
上面的測試結果以下:
toMatch
這個matcher
就是用來判斷字符串是否和toMatch
提供的模式匹配,以下:
// 字符串相關 test('toMatch', () => { const str = 'Lebron James' expect(str).toMatch(/Ja/) expect(str).toMatch('Ja') }) 複製代碼
matchers
可使用toContain
判斷數組或者集合是否包含某個元素,使用toHaveLength
判斷數組的長度,代碼以下:
test('Array Set matchers', () => { const arr = ['Kobe', 'James', 'Curry'] const set = new Set(arr) expect(arr).toContain('Kobe') expect(set).toContain('Curry') expect(arr).toHaveLength(3) }) 複製代碼
matchers
使用toThrow
來判斷拋出的異常是否符合預期:
function throwError () { throw new Error('this is an error!!') } test('toThrow', () => { expect(throwError).toThrow(/this is an error/) }) 複製代碼
jest
進階用法所謂分組測試,核心在於,將不一樣的測試進行分組,再結合勾子函數(生命週期函數),完成不一樣分組的定製化測試,以知足測試過程重的複雜需求。
咱們首先在src
下新建兩個文件hook.js
和hook.test.js
,這一部分代碼在這兩個文件中完成,首先直接給出hook.js
代碼
// hook.js export default class Count { constructor () { this.count = 2 } increase () { this.count ++ } decrease () { this.count -- } double () { this.count *= this.count } half () { this.count /= this.count } } 複製代碼
如今呢,咱們想要對Count
類的四個方法單獨測試,數據互相不影響,固然咱們能夠本身去直接實例化4
個對象,不過,jest
給了咱們更優雅的寫法---分組,咱們使用describe
函數分組,以下:
describe('分別測試Count的4個方法', () => { test('測試increase', () => { }) test('測試decrease', () => { }) test('測試double', () => { }) test('測試half', () => { }) }) 複製代碼
這樣咱們就使用describe
函數配合test
將測試分爲了四組,接下來,爲了能更好的控制每一個test
組,咱們就要用到jest
的勾子函數。 咱們這裏要介紹的是jest
裏的四個勾子函數beforeEach,beforeAll,afterEach,afterAll
。
顧名思義,beforeEach
是在每個test函數執行以前,會被調用;afterEach
則是在每個test函數執行以後調用;beforeAll
是在全部test函數執行以前調用;afterAll
則是在全部test函數執行以後調用。咱們能夠看下面這個例子:
import Count from "./hook" describe('分別測試Count的4個方法', () => { let count beforeAll(() => { console.log('before all tests!') }) beforeEach(() => { console.log('before each test!') count = new Count() }) afterAll(() => { console.log('after all tests!') }) afterEach(() => { console.log('after each test!') }) test('測試increase', () => { count.increase() console.log(count.count) }) test('測試decrease', () => { count.decrease() console.log(count.count) }) test('測試double', () => { count.double() console.log(count.count) }) test('測試half', () => { count.half() console.log(count.count) }) }) 複製代碼
輸出的結果如圖:
能夠看到,咱們在每一個test執行以前,beforeEach
裏面從新實例化了count
,因此每一次的count是不一樣的。合理的使用勾子函數,咱們能夠更好的定製測試。
在咱們前端開發過程當中,因爲javascript
是單線程的,異步編程是咱們開發人員常常要作的工做,而異步代碼也是最容易出錯的地方,對異步代碼邏輯進行測試,是頗有必要的,這一節將對jest
如何進行異步測試,作一個詳細的介紹。
setTimeout
開始咱們首先新建timeout.js,timeout.test.js
文件,timeout.js
文件代碼很簡單:
export default (fn) => { setTimeout(() => { fn() }, 2000) } 複製代碼
咱們如今的目標就是去測試,寫的這個函數,是否是會像咱們預期的那樣,傳入一個函數做爲參數(簡單爲主,沒有作參數校驗),2s
後,執行這個函數。
咱們的測試代碼(timeout.test.js
)以下:
import timeout from './timeout' test('測試timer', () => { timeout(() => { expect(2+2).toBe(4) }) }) 複製代碼
若是咱們運行這段測試代碼,必定是會經過的,可是,這真的表明咱們寫在timeout
裏的方法測試經過了嗎?咱們在timout.js
中打印輸出一段文字
export default (fn) => { setTimeout(() => { fn() console.log('this is timeout!') }, 2000) } 複製代碼
而後咱們運行測試代碼(npm test timeout.test
這樣只運行一個文件),你會發現,什麼打印內容都沒有輸出:
jest
在運行測試代碼,執行
test
方法時,從函數內部第一行執行到最後一行,當執行邏輯走到代碼塊最後一行時,沒有異常就會返回測試成功,這個過程當中
不會去等待異步代碼的執行結果
,因此咱們這樣的測試方法,無論
setTimeout
裏怎麼實現,回調函數裏怎麼實現,都不會執行回調函數內部的邏輯。
若是咱們須要測試代碼在真正執行了定時器裏的異步邏輯後,才返回測試結果,咱們須要給test
方法的回調函數傳入一個done
參數,並在test
方法內異步執行的代碼中調用這個done
方法,這樣,test
方法會等到done
所在的代碼塊內容執行完畢後才返回測試結果:
import timeout from './timeout' test('測試timer', (done) => { timeout(() => { expect(2+2).toBe(4) done() }) }) 複製代碼
done
參數以後,獲得了預期的結果,打印輸出了內容,證實咱們回調函數內的代碼執行了。
fakeTimers
提升測試效率咱們上一小節介紹瞭如何去測試寫在定時器裏異步代碼的執行,但這裏存在一個問題,好比,咱們的定時器可能須要幾十秒才執行內部邏輯(這雖然不多見,主要看業務需求),咱們的測試代碼也會好久纔會返回結果,這無疑大大的下降了開發測試效率。
jest
也考慮到了這一點,讓咱們可使用fakeTimers
模擬真實的定時器。這個fakeTimers
在遇到定時器時,容許咱們當即跳過定時器等待時間,執行內部邏輯,好比,對於剛剛的timeout.test
,咱們的測試代碼能夠作以下改變:
jest.fn()
生成一個jest
提供的用來測試的函數,這樣咱們以後回調函數不須要本身去寫一個jest.useFakeTimers()
方法啓動fakeTimers
jest.advanceTimersByTime()
方法,參數傳入毫秒時間,jest
會當即跳過這個時間值,還能夠經過toHaveBeenCalledTimes()
這個mathcer
來測試函數的調用次數。完整代碼以下:
test('測試timer', () => { jest.useFakeTimers() const fn = jest.fn() timeout(fn) // 時間快進2秒 jest.advanceTimersByTime(2000) expect(fn).toHaveBeenCalledTimes(1) }) 複製代碼
測試timer(12ms)
對比以前的
測試timer(2021ms)
,能夠看到,定時器的延遲時間,確實被跳過了,這提升了測試開發效率。
通過前面兩節的介紹,對於定時器這種異步場景的測試代碼編寫,實際上咱們已經掌握核心內容,這一節,咱們去探討一個更爲複雜的場景,那就是定時器嵌套。
咱們首先改造timout
裏的代碼以下:
export default (fn) => { setTimeout(() => { fn() console.log('this is timeout outside!') setTimeout(() => { fn() console.log('this is timeout inside!') }, 3000) }, 2000) } 複製代碼
按照上一小節的寫法,咱們的測試代碼能夠改造爲:
test('測試timer', () => { jest.useFakeTimers() const fn = jest.fn() timeout(fn) // 時間快進2秒 jest.advanceTimersByTime(2000) expect(fn).toHaveBeenCalledTimes(1) // 時間快進3秒 jest.advanceTimersByTime(3000) expect(fn).toHaveBeenCalledTimes(2) }) 複製代碼
其實也很簡單,就是在第一次2s
後,再過3s
後執行第二個定時器,此時fn
被調用了2
次,因此咱們只須要加上最後兩行代碼就能夠了。執行結果以下:
咱們能夠看到,兩條打印結果都輸出了。可是目前的這種實現不是很好,試想一下,若是這裏面的定時器嵌套比較多,或者咱們不清楚有幾個定時器,就會比較麻煩。jest
爲這種狀況提供了兩個有用的方法:
jest.runAllTimers()
這個方法就如同它的名字同樣,調用以後,會執行全部定時器,咱們的代碼能夠改造以下:
test('測試timer', () => { jest.useFakeTimers() const fn = jest.fn() timeout(fn) jest.runAllTimers() expect(fn).toHaveBeenCalledTimes(2) }) 複製代碼
jest
依舊快速的跳過了定時器等待時間。
jest.runOnlyPendingTimers()
這個方法的意思是,只執行當前正在等待的全部定時器,這個例子中,只有外層定時器是正在等待的,內層定時器只有在外層定時器執行時,才處於等待狀態,咱們改造測試代碼以下:
test('測試timer', () => { jest.useFakeTimers() const fn = jest.fn() timeout(fn) jest.runOnlyPendingTimers() expect(fn).toHaveBeenCalledTimes(1) }) 複製代碼
jest.runOnlyPendingTimers()
便可。
關於上述內容,有一點須要說明:
若是咱們編寫了多個test
函數,它們都使用fakeTimers
,必定要在beforeEach
勾子中每次都調用jest.useFakeTimers()
,不然,多個test
函數中的fakeTimers
會是同一個,將會互相干擾,產生不符合預期的執行結果
beforeEach(() => { jest.useFakeTimers() }) 複製代碼
(promise/async await)
promise
寫法在咱們前端開發中,經過請求後端接口獲取數據是很重要的一個流程,這一節主要就是介紹這個過程當中如何編寫測試代碼(實際上這裏的不少內容,以前介紹定時器的章節是有介紹過的)
爲了簡單起見,咱們使用axios(npm i axios)
這個成熟的庫來輔助咱們作數據請求。首先新建request.js, request.test.js
這兩個文件,在request.js
文件請求一個免費api:
import axios from 'axios' export const request = fn => { axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => { fn(res) console.log(res) }) } 複製代碼
咱們在request.test.js
中,爲了保證異步代碼執行完畢後結束測試,和以前介紹的同樣,在test
的回調函數中傳入done
參數,在回調函數裏執行done()
,代碼以下:
import { request } from './request' test('測試request', (done) => { request(data => { expect(data.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) done() }) }) 複製代碼
咱們如今改造一下request.js
的代碼,讓它返回一個promise
:
export const request = () => { return axios.get('https://jsonplaceholder.typicode.com/todos/1') } 複製代碼
爲了測試上述代碼,咱們request.test.js
也要作必定的修改:
test('測試request', () => { return request().then(data => { expect(data.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) }) }) 複製代碼
注意,上面的寫法不須要傳入done
參數了,可是,須要咱們使用return
返回,若是不寫return
,那jest
執行test
函數時,將不會等待promise
返回,這樣的話,測試結果輸出時,then
方法將不會執行。咱們能夠嘗試如下兩種寫法(改變"completed": true
),第一種寫法測試不會經過,第二種測試是能夠經過的(由於promise並無返回結果):
// 第一種 test('測試request', () => { return request().then(data => { expect(data.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": true }) }) }) 複製代碼
// 第二種 test('測試request', () => { request().then(data => { expect(data.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": true }) }) }) 複製代碼
上面的測試代碼,咱們也能夠寫成下面的形式:
test('測試request', () => { return expect(request()).resolves.toMatchObject({ data: { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false } }) }) 複製代碼
注意,resolves
將返回request
方法執行後全部返回內容,咱們使用toMatchObject
這個matcher
,當傳入的對象可以匹配到request
方法執行後返回對象的一部分鍵值對,測試就會經過。
async await
語法糖async await
本質上就是promise
鏈式調用的語法糖,咱們上一小節最後的測試代碼,若是使用async await
的方式去書寫,以下:
// 寫法一 test('測試request', async () => { const res = await request() expect(res.data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) }) // 寫法二 test('測試request', async () => { await expect(request()).resolves.toMatchObject({ data: { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false } }) }) 複製代碼
在咱們實際項目中,須要對這種接口請求作錯誤處理,一樣,也須要對異常狀況編寫測試代碼。
咱們首先在request.js
增長一個方法:
export const requestErr = fn => { return axios.get('https://jsonplaceholder.typicode.com/sda') } 複製代碼
這裏請求一個不存在的接口地址,會返回404
,因而咱們的的測試代碼爲:
test('測試request 404', () => { return requestErr().catch((e) => { console.log(e.toString()) expect(e.toString().indexOf('404') > -1).toBeTruthy() }) }) 複製代碼
jest
官網的一段說明:
catch
,jest不回去執行
catch
裏的內容,因此須要咱們去寫
expect.assertions(1)
這句話,表明,指望執行的斷言是1次,
catch
方法算一次斷言,因此,正常狀況,因爲不會執行
catch
,這裏會報錯(執行了0次斷言),當這裏報錯了,說明咱們的代碼也按照預期產生了異常。
這種寫法目前已經不須要了,詳細見removed useless expect.assertions,因此,如今就按照上面那種方式,直接書寫,測試經過表明確實如咱們預期的產生異常。
一樣的,咱們還可使用另外一種方式完成異常代碼測試:
test('測試request 404', () => { return expect(requestErr()).rejects.toThrow(/404/) }) 複製代碼
這裏的rejects
和上一節的resolves
相互對於,表明執行方法產生的錯誤對象,這個錯誤對象拋出404
異常(toThrow(/404/)
)
咱們一樣可使用async await
語法糖書寫異常測試的代碼:
test('測試request 404', async () => { await expect(requestErr()).rejects.toThrow(/404/) }) // 或者可使用try catch語句寫的更完整 test('測試request 404', async () => { try { await requestErr() } catch (e) { expect(e.toString()).toBe('Error: Request failed with status code 404') } }) 複製代碼
mock
)數據咱們首先新建mock.js, mock.test.js
文件
jest.fn()
模擬函數首先在mock.js
寫一個函數:
export const run = fn => { return fn('this is run!') } 複製代碼
實際上以前咱們已經使用過jest.fn()
了,這裏咱們更進一步的學習它。
fn()
函數能夠接受一個函數做爲參數,這個函數就是咱們想要jest.fn()
爲咱們mock
的函數,咱們編寫mock.test.js
:test('測試 jest.fn()', () => { const fn = jest.fn(() => { return 'this is mock fn 1' }) }) 複製代碼
jest.fn()
能夠初始化時候不傳入參數,而後經過調用生成的mock
函數的mockImplementation
或者mockImplementationOnce
方法來改變mock函數內容,這兩個方法的區別是,mockImplementationOnce
只會改變要mock
的函數一次:test('測試 jest.fn()', () => { const func = jest.fn() func.mockImplementation(() => { return 'this is mock fn 1' }) func.mockImplementationOnce(() => { return 'this is mock fn 2' }) const a = run(func) const b = run(func) const c = run(func) console.log(a) console.log(b) console.log(c) }) 複製代碼
this is mock fn 2
,以後都是
this is mock fn 1
一樣的,咱們可使用mock
函數的mockReturnValue
和mockReturnValueOnce(一次)
方法來改變函數的返回值:
test('測試 jest.fn()', () => { const func = jest.fn() func.mockImplementation(() => { return 'this is mock fn 1' }) func.mockImplementationOnce(() => { return 'this is mock fn 2' }) func.mockReturnValue('this is mock fn 3') func.mockReturnValueOnce('this is mock fn 4') .mockReturnValueOnce('this is mock fn 5') .mockReturnValueOnce('this is mock fn 6') const a = run(func) const b = run(func) const c = run(func) const d = run(func) console.log(a) console.log(b) console.log(c) console.log(d) }) 複製代碼
注意到,方法是能夠鏈式調用的,方便屢次輸出不一樣的返回值。
toBeCalledWith
這個matcher
來測試函數的傳參數是否符合預期:test('測試 jest.fn()', () => { const func = jest.fn() const a = run(func) expect(func).toBeCalledWith('this is run!') }) 複製代碼
不少時候,咱們在前端開發過程當中,後端接口尚未提供,咱們須要去mock
接口返回的數據。
咱們首先在mock.js
中編寫一個簡單的請求數據的代碼:
import axios from 'axios' export const request = fn => { return axios.get('https://jsonplaceholder.typicode.com/todos/1') } 複製代碼
接着,咱們在mock.test.js
中,使用jest.mock()
方法模擬axios
,使用mockResolvedValue
和mockResolvedValueOnce
方法模擬返回的數據,一樣的,mockResolvedValueOnce
方法只會改變一次返回的數據:
import axios from 'axios' import { request } from './mock' jest.mock('axios') test('測試request', async () => { axios.get.mockResolvedValueOnce({ data: 'Jordan', position: 'SG' }) axios.get.mockResolvedValue({ data: 'kobe', position: 'SG' }) await request().then((res) => { expect(res.data).toBe('Jordan') }) await request().then((res) => { expect(res.data).toBe('kobe') }) }) 複製代碼
咱們使用jest.mock('axios')
來使用jest
去模擬axios
,測試正確的經過了。
dom
相關測試dom
相關的測試其實很簡單,咱們首先新建dom.js, dom.test.js
兩個文件,代碼以下:
// dom.js export const generateDiv = () => { const div = document.createElement('div') div.className = 'test-div' document.body.appendChild(div) } // dom.test.js import { generateDiv } from './dom' test('測試dom操做', () => { generateDiv() generateDiv() generateDiv() expect(document.getElementsByClassName('test-div').length).toBe(3) }) 複製代碼
這裏只有一點要說明,jest
的運行環境是node.js
,這裏jest
使用jsdom
來讓咱們能夠書寫dom
操做相關的測試邏輯。
snapshot
)測試咱們若是沒有接觸過快照測試,可能會以爲這個名字很高大上。因此咱們首先新建snapshot.js, shapshot.test.js
來看看快照測試到底是什麼。
在咱們的平常開發中,總會寫一些配置性的代碼,它們大致不會變化,可是也會有小的變動,這樣的配置可能以下(snapshot.js
):
export const getConfig = () => { return { server: 'https://demo.com', port: '8080' } } 複製代碼
咱們的測試代碼以下:
import { getConfig } from './snapshot' test('getConfig測試', () => { expect(getConfig()).toEqual({ server: 'https://demo.com', port: '8080' }) }) 複製代碼
這樣咱們經過了測試。可是,假如後續咱們的配置改變了,我就須要同步的去修改測試代碼,這樣會比較麻煩,從而,jest
爲咱們引入了快照測試,先上測試代碼:
test('getConfig測試', () => { expect(getConfig()).toMatchSnapshot() }) 複製代碼
咱們運行測試代碼以後,會在項目根目錄下生成一個__snapshots__
文件夾,下面有一個snapshot.test.js.snap
快照文件,文件內容以下:
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`getConfig測試 1`] = ` Object { "port": "8080", "server": "https://demo.com", } `; 複製代碼
jest
會在運行toMatchSnapshot()
的時候,首先檢查有沒有這個快照文件,若是沒有,則生成,當咱們改動配置內容時,好比把port
改成8090
,再次運行測試代碼,測試不經過,結果以下:
npm test snapshot.test -- -u
,就能夠自動更新咱們的快照文件,測試再次經過,這就讓咱們不須要每次更改配置文件的時候,手動去同步更新測試代碼,提升了測試開發效率:
此時咱們的快照文件更新爲以下代碼:
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`getConfig測試 1`] = ` Object { "port": "8090", "server": "https://demo.com", } `; 複製代碼
jest
其它一些有用的知識jest
監聽文件變化這個功能很簡單,咱們只須要運行jest
命令的時候,後面加上--watch
便可,咱們在package.json
中新增一條命令:
"scripts": {
"test": "jest",
"test-watch": "jest --watch"
},
複製代碼
在新增完這條命令後,爲了能讓jest
能夠監聽文件變化,咱們還須要把咱們的代碼文件變成一個git
倉庫,jest
也正式依靠git
的能力實現監聽文件變化的,咱們運行git init
,接着咱們運行npm run test-watch
,在必定時間後,咱們開啓監聽模式,命令行最後幾行輸出應該是:
這裏對watch
模式的幾個有用功能作一個簡單介紹(也就是圖中英文說明):
a
鍵運行全部測試代碼f
鍵只運行全部失敗的測試代碼p
鍵按照文件名篩選測試代碼(支持正則)t
鍵按照測試名篩選測試代碼(支持正則)q
鍵盤推出watch
模式enter
鍵觸發一次測試運行這些我建議你們自行去嘗試,它們都是十分簡單好用的功能。
測試覆蓋率,簡單來講就是咱們業務代碼中,編寫測試代碼的比例,jest
給咱們提供了直接生成測試覆蓋率文件的方法,也就是運行jest
命令時後面加上--coverage
參數,咱們修改package.json
文件以下:
"scripts": {
"test": "jest",
"test-watch": "jest --watch",
"coverage": "jest --coverage"
},
複製代碼
接下來,運行npm run coverage
,咱們能夠看到命令行輸出以下:
coverage
文件夾:
index.html
,以下圖:
這裏對這個表格項目作一個簡單的說明:
Statements
是語句覆蓋率:表示代碼中有多少執行的語句被測試到了
Branches
是分支覆蓋率:表示代碼中有多少if else switch
分支被測試到了
Functions
是函數覆蓋率:表示代碼中被測試到的函數的佔比
Lines
是行覆蓋率:表示代碼中被測試到的行數佔比
咱們能夠利用生成的測試覆蓋率文件,更好的完善改進咱們的測試代碼。
jest.config.js
配置文件我對於學習一個工具的配置文件的建議是,首先按照默認的來,當你須要改變配置的時候,再去查閱官方文檔學習,不推薦去死記硬背。
我這裏也不會去介紹怎麼去配置jest
文件,咱們能夠經過jest初始化時候默認生成的那個jest.config.js
來學習(有詳細註釋),也能夠在官網中查閱相關的配置參數。
因爲篇幅緣由,不適合再介紹更多的信息,更多的api
相關的信息,建議去查閱官網來學習。
這篇文章我的認爲已經把jest
的基礎和最核心的內容作了闡述,可能咱們開發過程當中,使用react(enzyme), vue( @vue/test-utils)
這樣的開發框架,使用webpack
這樣的工程化工具,在使用jest
的時候,會結合使用一些開源庫,我相信學好了jest
自己以後,配置和使用它們都不會有太多困難。
最後,但願這篇文章能夠幫助到你們,感謝能看到這裏的每個小夥伴。