首先是為什么要寫單元測試? 主要還是測試我們代碼有沒有達到預期的效果,其次,如果嚴格按照TDD(測試驅動開發)來進行開發的話,我們還會更加注重產品細節,代碼可能更加健壯。因為TDD是測試放到第一位,寫代碼之前,先寫測試。測試怎么寫?肯定是思考產品的各種使用場景,以及在每種場景下,會有什么效果或異常,思考好了,測試寫完了,整個產品也就是一清二楚了。真正寫代碼的時候,只是把測試場景用代碼實現,這些所謂的場景就是一個個測試用例。
其次是怎么寫單元測試。測試的目的就是看看代碼有沒有達到我們的預期,那肯定是先引入要測試的代碼,然后是運行代碼,最后再和我們的預期做比較,如果和預期一致,就表明代碼沒有問題,如果沒有達到預期, 就是代碼有問題。和預期進行比較,就是斷言。這也就是意味着,每一個測試中最終都會落腳到一個斷言。如果沒有斷言(比較),測試也沒有意義,代碼運行完了,結果呢?不知道。斷言就是比較,判斷正不正確,對不對,1 + 1 是不是等於2, 就是一個最簡單的斷言,1+1是計算,程序的運行,2就是期望或預期,等於就是判斷或比較,具體到測試代碼(jest)中,就是expect(1+1).toBe(2). 在測試程序中, 只要看到expect 什么, to 什么,這就是斷言,還有可能是assert.
最后是運行測試代碼。單元測試寫了,肯定要運行,不運行,怎么知道結果。這里是我學習時最大的困惑,想明白了,有些問題也就迎刃而解了。當我們運行單元測試,就是在命令行中輸入jest的時候,實際上是在node 環境下執行js代碼。使用jest 進行單元測試,就是啟動了一個node程序來執行測試代碼。每當遇到測試問題的時候,先想一想這個前提,說不定,問題就解決了。
好了,說的也差不多了,那就一起來學習一下Jest 吧。Jest有一個好處,就是不用配置也能用,開箱即用,它提供了斷言,函數的mock等常用的測試功能。npm install jest --save-dev, 安裝jest 之后,就可以使用它進行單元測試了。打開git bash,輸入mkdir jest-test && cd jest-test && npm init -y, 新建jest-test 項目並初始化為node項目。再npm install jest --save-dev, 安裝jest ,現在就可以進行測試了。先從簡單的函數測試開始學起。touch func.js, 新建一個func.js 文件,暴漏一個greeting 函數,注意使用commonJs 的格式,因為jest 是在node環境下運行的,node暫時沒有實現ES6 module
function greeting(guest) { return `Hello ${guest}`; }
函數寫完了,那怎么測試呢,測試代碼放到什么地方呢?Jest識別三種測試文件,以.test.js結尾的文件,以.spec.js結尾的文件,和放到__tests__ 文件夾中的文件。Jest 在進行測試的時候,它會在整個項目進行查找,只要碰到這三種文件它都會執行。干脆,再寫兩個函數,用三種測試文件分別進行測試, func.js 如下
function greeting(guest) { return `Hello ${guest}`; } function createObj(name, age) { return { name, age } } function isTrueOrFasle(bool) { return bool } module.exports = { greeting, createObj, isTrueOrFasle }
新建greeting.test.js測試greeting 函數,createObj.spec.js來測試createObj函數,新建一個__tests__ 文件夾,在里面建一個isTrue.js 來測試isTrueOrFalse 函數。 具體到測試代碼的書寫,jest 也有多種方式,可以直接在測試文件中寫一個個的test或it用來測試,也可以使用describe 函數,創建一個測試集,再在describe里面寫test或it , 在jest中,it和test 是一樣的功能,它們都接受兩個參數,第一個是字符串,對這個測試進行描述,需要什么條件,達到什么效果。第二個是函數,函數體就是真正的測試代碼,jest 要執行的代碼。來寫一下greeting.test.js 文件,greeting 函數的作用就是 傳入guest參數,返回Hello guest. 那對應的一個測試用例就是 傳入sam,返回Hello sam. 那描述就可以這么寫, should return Hello sam when call greeting with param sam, 具體到測試代碼,引入greeting 函數,調用greeting 函數,傳入‘sam’ 參數, 作一個斷言,函數調用的返回值是不是等於Hello sam. greeting.test.js 如下
const greeting = require('./fun').greeting; test('should return Hello sam when input sam', () => { let result = greeting('sam'); expect(result).toBe('Hello sam'); })
這和文章開始說的一樣,測試的寫法為三步,引入測試內容,運行測試內容,最后做一個斷言進行比較,是否達到預期。Jest中的斷言使用expect, 它接受一個參數,就是運行測試內容的結果,返回一個對象,這個對象來調用匹配器(toBe) ,匹配器的參數就是我們的預期結果,這樣就可以對結果和預期進行對比了,也就可以判斷對不對了。按照greeting測試的寫法,再寫一下createObj的測試,使用it
const createObj = require('./fun').createObj; it('should return {name: "sam", age: 30} when input "sam" and 30', () => { let result = createObj('sam', 30); expect(result).toEqual({name: 'sam', age: 30}); // 使用toEqual })
最后是isTrueOrFalse函數的測試,這里最好用describe(). 因為這個測試分為兩種情況,一個it 或test搞不定。對一個功能進行測試,但它分為多種情況,需要多個test, 最好使用descibe() 把多個test 包起來,形成一組測試。只有這一組都測試完成之后,才能說明這個功能是好的。它的語法和test 的一致,第一個參數也是字符串,對這一組測試進行描述, 第二個參數是一個函數,函數體就是一個個的test 測試。
const isTrueOrFasle = require('../fun').isTrueOrFasle; describe('true or false', () => { it('should return true when input true', () => { let result = isTrueOrFasle(true); expect(result).toBeTruthy(); // toBeTruthy 匹配器 }) test('should return false when input fasle', () => { let result = isTrueOrFasle(false); expect(result).toBeFalsy(); // toBeFalsy 匹配器 }) })
三個測試寫完了,那就運行一下,看看對不對。把package.json中的scripts 的test 字段的值改成 'jest', 然后npm run test 進行測試, 可以看到三個測試都通過了。 修改一下,讓一個測試不通過,比如isTrue.js中把第一個改成false,
it('should return true when input true', () => { let result = isTrueOrFasle(false); expect(result).toBeTruthy(); // toBeTruthy 匹配器 })
再運行npm run test ,
可以看到失敗了,也指出了失敗的地方,再看一下它的描述,它把組測試放到前面,后面是一個測試用例的描述,這樣,我們就很輕松看到哪一個功能出問題了,並且是哪一個case. 這也是把同一個功能的多個test case 放到一起的好處。
我們再把它改回去,再執行npm run test,如果這樣改動測試,每一次都要執行測試的時候,使用npm run test就有點麻煩了,jest 提供了一個watchAll 參數,會對測試文件以及測試文件引用的源文件進行實時監聽,如果有變化,立即進行測試。package.json中的 test 改成成jest --watchAll
"scripts": { "test": "jest --watchAll" }
npm run test, 就可以啟動jest 的實時測試了。當然你也可以隨時停止掉,按q 鍵就可以。
jest 的基本測試差不多了,再來看看它的異步代碼的測試, 先把所有的測試文件刪掉,再新建一個func.test.js 文件,現在就只有func.js 和 func.test.js 了。處理異步或是用回調函數, 或是promise 。
回調函數
最常見的回調函數就是ajax請求,返回數據后執行成功或失敗的回調。在Node 環境下,有一個npm 包request, 它可以發送異步請求,返回數據后調用回調函數進行處理,npm i request --save, 安裝一下,然后func.js 修改如下
const request = require('request'); function fetchData(callback) { request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) { callback(body); }); } module.exports = fetchData;
那怎么測試?肯定調用fetchData, 那就先要創建一個回調函數傳給它,因為fetchData獲取到數據后,會調用回調函數,那就可以在回調函數中創建一個斷言,判斷返回的數據是不是和期望的一樣。func.test.js 文件修改為如下測試代碼。
const fetchData = require('./fun'); test('should return data when fetchData request success', () => { function callback(data) { expect(data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) } fetchData(callback); })
執行npm run test 命令,看到pass,但其實並沒有達到預期的效果,在callback 函數中加入console.log(data) 試一試,就知道了。測試文件改變后,jest 會重新跑一遍(開啟了watch),但並沒有打印出data,也就是說callback 函數並沒有被調用。這是為什么,我在這個地方想了好久,主要還是對異步了解的不夠。當執行jest 測試的時候,實際上是node 執行test函數體的代碼,首先看到callback的函數聲明,它聲明函數,然后看到fetchData() ,它就調用這個函數,請求https://jsonplaceholder.typicode.com/todos/1 接口,這個時候,getTodo函數就執行完了。你可能會想,回調函數都沒有執行,這個函數怎么算執行完了呢?回調函數並不是代碼執行的,而是放到node的異步隊列中被執行的。異步的請求,可以看作是一個對話,
執行fetchData: " hi, node, 你幫我執行一個請求,如果請求成功,就執行這個回調函數"
node: "好,我幫你請求”
然后node 就請求了,然后實時監聽請求的狀態,如果返回數據,它就把回調函數插入到它的異步隊列中。Node的事件循環機制,就把這個函數執行了。
這時再看異步函數,其實,異步函數的作用,只是一個告知的作用,告知環境來幫我做事情,只要告知了,函數就算執行完了,其它剩下的事情,請求接口,執行回調函數,就是環境的事了。
只要一告知,getTodo 函數就執行完了,繼續向下執行,由於函數的執行是該測試的最后一行代碼,它執行完之后,這個測試就執行完了,沒有錯誤,jest 就pass了. 但是該測試並沒有覆蓋到callback函數的調用,實際上在背后,node是幫我們發送請求,執行callback 的。這也就是官網說的,By default, Jest tests complete once they reach the end of their execution. That means this test will not work as intended: The problem is that the test will complete as soon as fetchData
completes, before ever calling the callback. 那怎么辦,官方的建議是使用done. 就是test的第二個參數接受一個done, 然后在callback 里面加done(), 如下所示
test('should return data when fetchData request success', (done) => { function callback(data) { console.log(data); expect(data).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) done(); } fetchData(callback); });
測試又重新跑了一遍,還是報錯了,不過是斷言寫錯了,表明callback 調用了,達到了預期的效果。data 是一個字符串,toEqual了一個對象,所以測試失敗了。Json parse 一下data 就可以了。
expect(JSON.parse(data)).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false })
測試通過了。思考一下done 的作用,加上done 表示等待,jest 在執行單元測試的時候,如果它看到一個測試用例(test) 的第二個函數參數有一個done參數, 它就知道,這個測試用例,執行完最后一行代碼的時候,還不算測試完成,還要等待,等待done函數的執行,只有done函數執行了,這個測試用例才算完成,才能執行下一個test. 如果done函數沒有調用,它就不能執行下一個測試(test)。加上done 以后,一個測試有沒有執行完的依據就不是執行到最后一行代碼,而是看done() 有沒有被調用。也就是說,done 改變了測試的默認行為,因為默認情況下,只要執行到一個測試的最后一行代碼,就認為個測試執行完了,就要執行下一個測試,但有了done就不一樣了,它執行完這一行代碼后,並不會繼續執行下一個測試,而是等待,等待done() 函數的執行,只有done()函數執行了,它才會執行下一個測試。具體到這個測試用例,當jest執行到fetchData(callback) 這一行代碼的時候,他就暫停執行了,這時node 就會在背后發送請求,請求成功后,把回調函數放到異步隊列中,node的事件循環機制就會執行這個回調函數,而回調函數中正好有done(), done() 函數執行了,這時jest 看到done函數執行了,就把測試置為pass, 然后執行下一個測試。當然jest也不會一直等着,默認是5s,如果5s后done 還沒有執行,它就執行下一個測試,這也表明測試失敗了。有時,網落太慢或沒有網的時候, 你再跑這個測試,你會現如下錯誤
這就是5s內沒有調用 done() 測試失敗的例子。
總結一下,異步回調函數的測試,一個是使用done作為參數,一個是調用done,在測試的某個地方一定要觸發或者調用done()。done是針對一個個test測試用例而言的,目的就是告訴jest一個個test 測試真正完成依據是什么。如果一個測試有done參數,就表示這個測試完成的依據是done的調用,執行到測試的最后一行代碼,也不算完事,只有done調用了,才算完事,沒有辦法,jest在執行這個測試的時候,就只能等待done的調用,只有調用了,它才會執行一個測試。如果一個測試沒有done參數,那么這個測試的完成的依據就是執行完最后一行代碼,執行完最后一行代碼,jest就可以執行下一個測試了。 當然也不會一直等待,默認是5s。
Promise
Promise 相對好測試一點,因為promise 可以使用then的鏈式調用。只要等待它的resolve, 然后調用then 來接受返回的數據進行對比就可以了,如果沒有resolve 肯定是失敗了。等待resolve,在測試中是使用的return, return Promise 的調用,就是等待它的resolve. 把fetchData 函數轉化成使用promise 進行請求,func.js如下
const request = require('request'); function fetchData() { return new Promise((resolve, reject) => { request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) { if (error) { reject(error); } resolve(body); }); }) } module.exports = fetchData;
測試函數(func.test.js)改為
test('should return data when fetchData request success', () => { return fetchData().then(data => { expect(JSON.parse(data)).toEqual({ "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }) }) });
進行promise測試的時候,在測試代碼中,一定要注意使用return, 如果沒有return,就沒有等待,沒有等待,就沒有resolve,then 也就不會執行了,測試效果達不到。如果你想測試error, 把測試代碼改成error ,
test('should return err when fetchData request error', () => { return fetchData() .catch(e => { expect(e).toBe('error') }) });
jest 顯示pass, 但這個error 的測試並沒有執行,因為 fetchData 返回數據了,沒有機會執行catch error。按理說,這種情況要顯示fail,表示沒有執行到。怎么辦,官網建議使用expect.assertions(1); 在測試代碼之前,添加expect.assertions(1);
test('should return err when fetchData request error', () => { expect.assertions(1); // 測試代碼之前添加 return fetchData() .catch(e => { expect(e).toBe('error') }) });
這時jest 顯示fail 了。expect.assertions(1); 就是明確告訴jest, 在執行這個測試用例的時候,一定要做一次斷言。后面的數字是幾,就表明在一個test中,一定要做幾次斷言,如果沒有執行catch,也就沒有執行斷言,和這里的1,要做一次斷言不符,測試失敗,也就達到了測試的目的。
對於promise的測試,還有一個簡單的方法,因為promise 只有兩種情況,一個是fullfill, 一個是reject,expect() 方法返回的對象提供了resolves 和rejects 屬性,返回的就是resolve和reject的值,可以直接調用toEqual等匹配器。看一下代碼就知道了
test('should return data when fetchData request success', () => {
return expect(fetchData()).resolves.toMatch(/userId/); // 直接在expect函數中調用 fetchData 函數 }); test('should return err when fetchData request error', () => { return expect(fetchData()).rejects.toBe('error'); });
除了使用then的鏈式調用,還可以用async/await 對promise 進行測試,因為 await后面的表達式就是promise. 這時test的函數參數之前要加上async 關鍵字了。
test('should return data when fetchData request success', async () => { let expectResult = { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }; let data = await fetchData(); expect(JSON.parse(data)).toBe(expectResult); // 直接在expect函數中調用 fetchData 函數 }); test('should return err when fetchData request error', async () => { expect.assertions(1); try { await fetchData(); } catch (e) { expect(e).toBe('error'); } });
當然,也可以把async/await 與resolves 和rejects 相結合,
test('should return data when fetchData request success', async () => { await expect(fetchData()).resolves.toMatch(/userId/); // 直接在expect函數中調用 fetchData 函數 }); test('should return err when fetchData request error', async () => { await expect(fetchData()).rejects.toBe('error'); });
Mock 函數
有時你會發現,進行單元測試時,要測試的內容依賴其他內容,比如上面的異步請求,依賴網絡,很可能造成測試達不到效果。 能不能把依賴變成可控的內容?這就用到Mock。Mock就是把依賴替換成我們可控的內容,實現測試的內容和它的依賴項隔離。那怎么才能實現mock呢?使用Mock 函數。在jest中,當我們談論Mock的時候,其實談論的就是使用Mock 函數代替依賴。Mock函數就是一個虛擬的或假的函數,所以對它來說,最重要的就是實現依賴的全部功能,從而起到替換的作用。通常,mock函數會提供以下三個功能,來實現替換
函數的調用捕獲,設置函數返回值,改變原函數的實現。
怎樣創建Mock 函數呢?在jest 創建一個Mock 函數最簡單的方法就是調用jest.fn() 方法。
const mockFunc = jest.fn();
函數的調用捕獲指的是這個函數有沒有被調用,調用的參數是什么,返回值是什么,通常用於測試回調函數,mock 真實的回調函數。就像官網舉的forEach函數,它接受一個回調函數,每個調用者都會傳遞不同的回調函數過來,我們事先並不知道回調函數,再者我們測試forEach 的重點是,該函數是不是把數組中的每一項都傳遞給回調函數了,所以只要是一個函數就可以了,但該函數必須把調用的信息都保存下來,這就是Mock 函數的調用捕獲,為此mock 函數還有一個mock 屬性。在func.test.js 中,聲明forEach, 然后寫一個test測試,測試中就使用jest.fn() 生成的mock 函數來mock 真實的回調函數。
const forEach = (array, callback) => { for (let index = 0; index < array.length; index++) { const element = array[index]; callback(element); } } test('should call callback when forEach', () => { const mockFun = jest.fn(); // mock 函數 const array = [1, 2]; forEach(array, mockFun); // 用mock函數代替真實的回調函數 console.log(mockFun.mock); expect(mockFun.mock.calls.length).toBe(2) })
也打印出來mock函數mockFun的mock 屬性
calls 保存的就是調用狀態,results保存的就是返回值。calls 是一個數組,每一次的調用都組成數組的一個元素,在這里調用了兩次,就有兩個元素。每一個元素又是一個數組,它則表示的是函數調用時的參數,因為每次的調用都傳遞了一個參數給函數,所以數組只有一項。如果有多個參數,數組就有多項,按照函數中的參數列表依次排列。這時候,就可以做斷言,函數調用了幾次,就判斷calls.length. expect(mockFun.mock.calls.length).toBe(2) 就是斷言函數是不是調用了兩次。expcet(mockFun.mock.calls[0][0]) .toBe(1)就是斷言第一次調用的時候傳遞的參數是不是1. 可能覺得麻煩了, 的確有點麻煩了,幸好,jest 對函數的mock參數進行了簡單的封裝,提供了簡單的匹配器, toHaveBeenCalled(), toHaveBeenCalledTimes() ,使用起來有點方便了。你可能見過toBeCalled(), 其實,它和toHaveBeenCalled() 功能是一模一樣的,使用哪個都行。
函數返回值。有的時候,你不想調用函數,直接獲取到函數的返回值就可以了,比如異步函數, 以fetchData 為例,它直接返回一個promise 就好了,根本沒有必要請求服務器。mock函數有mockReturnValue(), 它的參數就是返回值。不過它不能返回promise. 可以使用mockResolvedValue直接返回promise的值. 對fetchData 進行mock, 然后設置它的mockResolvedValue()
const fetchData= jest.fn(); // fetchData.mockReturnValue("bar"); fetchData.mockResolvedValue({name:'sam'});
當我們調用fetchData函數, 它就會返回{name: 'sam'}. 但這時又會發現另外一個問題,fetchData 是從外部組件引入來的,無法在func.test.js 中直接mock. 我們要先引入fetchData,測試fetchData 的時候,就可以這么寫。引入fetchData, 然后讓fetchData = jest.fn() 進行mock , 然后使用mockResolvedValue ()設置返回值, fetchData.mockResolvedValue ({name: 'sam'}), fetchData測試如下
引入fetchData並mock, 又叫mock module, mock 一個模塊。 當然這也只是一種實現方式, 引入一個模塊,然后對這個模塊暴露出來函數依次進行mock, 當我們測試的時候,調用模塊暴露的函數就變成了調用mock函數,這也相當於mock了整個模塊。但是如果一個模塊暴露出很多函數,那么引入並mock的方式,就有點麻煩了。比如func.js 再暴露出三個簡單 的方法。
const request = require('request'); exports.fetchData = function fetchData() { return new Promise((resolve, reject) => { request('https://jsonplaceholder.typicode.com/todos/1', function (error, response, body) { resolve(body); }); }) } exports.add = (a, b) => a + b; exports.subtract = (a, b) => a -b; exports.multiply = (a, b) => a * b;
引入並mock 的方式就變成了
let func = require('./func'); func.add = jest.fn(); func.subtract = jest.fn(); func.fetchData = jest.fu(); func.multiply = jest.fu();
有點麻煩,不過jest 提供了一個mock()方法,第一個參數是要mock的模塊,可以自動mock這個模塊。自動mock這個模塊是什么意思呢?就是把模塊暴露出來的方法,全部自動轉換成mock函數jest.fn().(automatically set all exports of a module to the Mock Function). jest.mock('./func'), 就相當於把func.js 變成了
exports.fetchData = jest.fn();
exports.add = jest.fn(); exports.subtract = jest.fn(); exports.multiply = jest.fn();
不用每個函數都單獨mock,方便多了。當我們再require('./func.js')的時候,就require 這個mock 函數了, 測試函數就變成了如下內容,不過要注意,先mock 模塊,再require引入。
改變函數實現。 有時你不想使用默認的mock函數jest.fn(),尤其是測試回調函數的時候,你想提供回調函數實現,比如上面的forEach, 確實寫一個真實的回調函數進行測試,心里更有底一點。mock 函數實現也有兩種方法,jest.fn() 可以接受一個參數,這個參數就可以是一個函數實現。forEach 中的mock 函數就可以成
const mockFun = jest.fn(x => x + 1);
再調mockFun的時候, 實際上調的是x => x + 1; 函數。forEach 的測試修改一個
test('should call callback when forEach', () => { const mockFun = jest.fn(x => x + 1); const array = [1, 2]; forEach(array, mockFun); // 用mock函數代替真實的回調函數 console.log(mockFun.mock); expect(mockFun.mock.calls.length).toBe(2) })
可以看到有返回值了,測試正是調用了x =>x +1 函數實現
但是當我們使用jest.mock() 來mock一個模塊的時候,jest 已經把所有的方法自動mock成jest.fn(),無法給它傳參,也就無法提供實現了。這就要用到第二種mock實現方法了,mock 函數提供了一個方法mockImplementation(), 它的參數也是一個函數實現,使用mockImplementation() 來mock fetchData,讓它返回{name: 'sam'}
fetchData的整個測試
jest.mock('./func.js'); let fetchData = require('./func').fetchData; test('should return data when fetchData request success', () => { fetchData.mockImplementation(() => { return Promise.resolve({name: 'sam'}) }) return fetchData().then(res => { expect(res).toEqual({name: 'sam'}) }) })
以上jest.mock() 是mock 自己的module, 第三方模塊比如request, 要怎么mock啊? 方法是一樣,就是mock 的第一個參數要改一下,直接寫要mock 的第三方模塊名,它會從node modules 里面去找,然后自動mock. jest.mock('request'), request 模塊暴露出來的就是jest.fn(). 如果不確定jest.mock 第三方模塊的時候,發生了什么,我們可以先mock, 再require, 最后console.log 一下,還是拿request 為例,按照三步走,代碼如下,
jest.mock('request'); const request = require('request'); console.log(request);
可以看到不光整個模塊被mock了,就連里面的方法也被mock了。這時再使用request, 就是使用的mock的 request了,mock 函數的所有用法都是可以使用了,比如按照request 真實的使用方法,提供一個實現。現在就可以換一種思路來mock fetchData,由於fetchData調用request,我們mock request, fetchData就不用mock了。在test 文件mock request 並提供實現,這時fetchData調用的就是mock的request了。
jest.mock('request'); const request = require('request'); request.mockImplementation((url, callback) => { callback(null, 'ok', {name: 'sam'}) }) const fetchData = require('./func').fetchData; test('should return data when fetchData request success', () => { return fetchData().then(res => { expect(res).toEqual({name: 'sam'}) }) })
對於這種簡單的mock, jest.mock() 還提供了第二種寫法,它的第二個參數是一個函數,返回一個mock 實現
jest.mock('request', () => { return (url, callback) => { callback(null, 'ok', {name: 'sam'}) } }); const fetchData = require('./func').fetchData; test('should return data when fetchData request success', () => { return fetchData().then(res => { expect(res).toEqual({name: 'sam'}) }) })
還有一種mock實現的方式,jest.spyOn(), 它接受兩個參數,一個是對象,一個是對象上的某一個方法,返回一個mock函數,使用jest.spyOn() mock add方法,
使用spyOn 進行mock的好處是在同一個test 下,它可以restore, 恢復到以前默認mock的狀態。這樣就不用寫beforeEach 和aftereEach 函數了。
const math = require('./func');
test("calls math.add", () => { const addMock = jest.spyOn(math, "add"); // override the implementation addMock.mockImplementation(() => "mock"); expect(addMock(1, 2)).toEqual("mock"); // restore the original implementation addMock.mockRestore(); expect(addMock(1, 2)).toBeUndefined(); });
當然,spyOn 還有另外一個功能,就是監聽函數,有時我們並不想mock 函數,改變函數的實現,只想監聽一下它有沒有被調用。
const math = require('./func'); test('should call add', () => { function callMath(a, b) { return math.add(a + b); } const addMock = jest.spyOn(math, 'add'); callMath(1, 2); expect(addMock).toBeCalled(); // toBeCalled, 就是函數有沒有被調用。 })
鈎子函數
既然上面提了一個beforeAll, beforeEach, 就簡單提一下Jest 的鈎子函數,它的作用相對簡單一點,就是做測試前的准備工作或測試后的清理工作。看名字也能知道它們的作用,beforeAll, 在所有測試之前做什么,beforeEach 在每一個測試之前做什么。確實會有這樣的需求,比如每次測試這前都要把值恢復到初始狀態。我做過這樣的一個測試
if(window.unicode || window.local || window.isEnable) { window.history.pushState('', '', '/uat=true') }
要做三種情況的測試,所以每一個測試之前beforeEach, 我都把值設為了false
beforeEach(() => { window.unicode = false; window.local = false; window.isEnable = false })
要注意的是,這里的beforeEach, beforeAll 等,都是根據describe 來的,describe 表示一組測試,如果沒有describe,那整個test文件就是一個describe.
describe('method called', () => { beforeEach(() => { window.unicode = false; window.local = false; window.isEnable = false }) describe('another beforeEach', () => { beforeEach(() => { window.unicode = false; window.local = false; window.isEnable = false }) }) })
但有時,在做初始化的時候,並沒有使用beforeEach, 但也沒有什么問題,確實如此,但當describe 嵌套太多的時候,有可能就會出問題,使用console.log 輸出一下,看一下執行順序,就知道了。
describe('method called', () => { console.log("before each outer outer") beforeEach(() => { console.log('before each outer inner') window.unicode = false; window.local = false; window.isEnable = false }) describe('another beforeEach', () => { console.log('before each inner outer'); beforeEach(() => { console.log('beforeEach inner inner'); window.unicode = false; window.local = false; window.isEnable = false }) }) })
先輸出before each outer outer, 再輸出了before each inner outer, 可以看到,它把describe 下面所有的沒有在鈎子函數里面的語句先執行了,然后再執行鈎子函數,而不是按照書寫的順序進行執行,一定要注意,最好還是把所有的初始化工作放到鈎子函數中.
Jest 在進行單元測試的時候,還可以生成測試代碼覆蓋率的報告,只要在run jest 的時候,提供一個參數coverage。按q 退出watch模式,輸入npx jest --coverage, 可以看到
同時在根目錄下,生成了一個測試coverage 目錄,在lcov-report下, 有一個index.html 文件,打開,可以看到有測試了哪些文件或目錄,點擊目錄,可以看到具體的文件,打開測試文件以后,在每一行前面會標有1x, 6x 等等,這表示這行代碼執行了多小次。在代碼內容上,它還有 一些標識, 比如 黑色的方塊I,E, 還有黃色的標識,這都表示這個branch 沒有測試,標紅的代碼則是直接沒有測試到,需要我們去覆蓋。
Jest的基本內容說的差不多,最后再說一個babel 配置。由於Jest 默認是commonJs 規范,而我們平時用的最多的確是ES module, import 和export。 這就需要在進行單元測試之前進行轉化,ES6 語法的轉化,肯定是使用babel。安裝babel, npm i @babel/core @babel/preset-env --save-dev 並在根目錄下配置babel.config.js, babel-jest 不用安裝了,安裝jest的時候,已經自動安裝了。
module.exports = { presets: [ [ '@babel/preset-env', { targets: { node: 'current', }, }, ], ], };
這時Jest 在進行單元測試的時候,就會自動轉化node 不認識的語法。