Jest 寫前端單元測(cè)試入門
???
本文轉(zhuǎn)載自微信公眾號(hào)「前端GoGoGo」,作者Joel 。轉(zhuǎn)載本文請(qǐng)聯(lián)系前端GoGoGo公眾號(hào)。
寫測(cè)試能減少 bug,提高代碼質(zhì)量。同時(shí),重構(gòu)測(cè)試覆蓋率高的代碼,不擔(dān)心改壞以前的功能。
前端的測(cè)試可以分為 3 類:?jiǎn)卧獪y(cè)試,集成測(cè)試 和 UI 測(cè)試。
- 單元測(cè)試是對(duì)軟件的最小單元進(jìn)行測(cè)試。比如:一個(gè)函數(shù),一個(gè)組件。
- 集成測(cè)試,也叫組裝測(cè)試或聯(lián)合測(cè)試。在單元測(cè)試的基礎(chǔ)上,將所有模塊按照設(shè)計(jì)要求組裝成為子系統(tǒng)或系統(tǒng),進(jìn)行測(cè)試。
- UI 測(cè)試是對(duì) UI 的呈現(xiàn)效果和交互進(jìn)行測(cè)試。
本文主要介紹用 Jest[1] 來寫單元測(cè)試。Jest 是一款優(yōu)雅、簡(jiǎn)潔的 JavaScript 測(cè)試框架。下面的是用 Jest 寫的單元測(cè)試:
import sum from './sum'; test('sum', () => { expect(sum(1, 2)).toBe(3); expect(sum(-1, -2)).toBe(-3); });
sum 是要測(cè)試的函數(shù)。test(...) 是包裹了要測(cè)試的一個(gè)特性。expect(...) 是斷言:期望 sum(1, 2) 的值是 3,如果 sum(1, 2) 的值不是 3,則該測(cè)試會(huì)失敗。
Jest 主要包含 3 塊內(nèi)容:
- 安裝。
- 運(yùn)行。
- Jest API。
1 安裝
Jest 是依賴 Node.js[2] 的。安裝好 Node 后,初始化 node 項(xiàng)目:
npm init -y
安裝 Jest
npm install --save-dev jest
支持 Babel
需要支持 ES6, ES7 等語法,則安裝 Babel 相關(guān)依賴:
yarn add --dev babel-jest @babel/core @babel/preset-env
在項(xiàng)目根目錄下創(chuàng)建 Babel 配置文件:babel.config.js 來配置與當(dāng)前 Node 版本兼容的 Babel:
module.exports = { presets: [['@babel/preset-env', {targets: {node: 'current'}}]], };
支持 TypeScript
yarn add --dev @babel/preset-typescript
在項(xiàng)目根目錄下創(chuàng)建 TypeScript 配置文件:tsconfig.json 。內(nèi)容類似:
{ "compilerOptions": { "module": "commonjs", "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, "sourceMap": true, "esModuleInterop": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules" ] }
2 運(yùn)行
運(yùn)行項(xiàng)目里的所有測(cè)試用例
需要在 package.json 中加如下內(nèi)容:
{ "scripts": { "test": "jest" } }
測(cè)試用例要寫在單獨(dú)的文件,而不是放在要測(cè)試的文件里。測(cè)試用例的文件名需要帶 .spec.[js|ts] 或 .test.[js|ts]。比如,一個(gè)文件叫 sum.js,對(duì)其進(jìn)行測(cè)試,一般在該文件的相同目錄下,創(chuàng)建個(gè)測(cè)試用例文件 sum.spec.js。
執(zhí)行 npm run test 即可運(yùn)行項(xiàng)目中,就會(huì)運(yùn)行所有的測(cè)試用例。
運(yùn)行特定的一個(gè)測(cè)試用例文件
在項(xiàng)目根目錄行執(zhí)行 yarn jest 測(cè)試用例文件路徑 或 npm run test 測(cè)試用例文件路徑。
運(yùn)行特定的一個(gè)測(cè)試用例
只需將要運(yùn)行用例的 test(...) 改成 test.only(...),然后再運(yùn)行該測(cè)試用例文件。類似的,運(yùn)行一組用例,把 describe(...) 改成 describe.only(...)。
獲取測(cè)試覆蓋率
測(cè)試覆蓋率(test coverage) 衡量的是功能代碼被測(cè)試用例覆蓋的比率。對(duì)代碼質(zhì)量要求高的項(xiàng)目,會(huì)要求測(cè)試覆蓋率在 90% 以上。
獲取測(cè)試覆蓋率的命令:
jest --coverage
3 Jest API
斷言 API
編寫測(cè)試時(shí),我們總是會(huì)做出一些假設(shè),斷言就是用于在代碼中捕捉這些假設(shè)。比如:
expect(2 + 2).toBe(4)
Jest 支持主要斷言API如下:
???
所有的斷言API見:這里[3]。
Jest 場(chǎng)景
測(cè)試異步代碼
處理異步一般有 3 種方式:回調(diào), Promise 和 Async/Await。
回調(diào)
業(yè)務(wù)代碼:
function fetchNameCallback(cb: (name: string) => void) { setTimeout(() => { cb('Joel'); }, 1000) }
用例代碼如下:
test('async: callback', done => { fetchNameCallback(name => { expect(name).toBe('Joel'); done(); // 通知 Jest,回調(diào)結(jié)束了 }); });
注意:異步回調(diào)結(jié)束后,需要調(diào)用參數(shù) done。以此來通知 Jest,回調(diào)結(jié)束了。
Promise
業(yè)務(wù)代碼:
function fetchName(throwError?: boolean) { return new Promise((resolve, reject) => { setTimeout(() => { if(!throwError) { resolve('Joel') } else { reject('error happened') } }, 1000) }); }
用例代碼如下:
test('async: Promise', () => { fetchName().then(name => { expect(name).toBe('Joel'); }); // 異常處理 fetchName(true).catch(e => { expect(e).toMatch('error'); }); });
Async/Await
業(yè)務(wù)代碼和上面的 Promise 的相同。
用例代碼如下:
test.only('Async/Await', async () => { const name = await fetchName(); expect(name).toBe('Joel'); // 異常處理 try { fetchName(true); } catch(e) { expect(e).toMatch('error'); } });
測(cè)試前的準(zhǔn)備操作和測(cè)試后的整理操作
寫測(cè)試的時(shí)候可能需要在測(cè)試前做一些準(zhǔn)備工作。運(yùn)行測(cè)試后,需要做進(jìn)行一些整理工作。用 Jest 這么寫:
每次用例的前后都執(zhí)行
// 前 beforeEach(() => { }); // 后 afterEach(() => { });
所有用例的前后執(zhí)行
beforeAll(() => { }); afterAll(() => { });
只會(huì)被執(zhí)行一次。
Mock 外部依賴
我們?cè)跍y(cè)試模塊功能時(shí),如果模塊對(duì)外部的依賴的是有問題的,也會(huì)導(dǎo)致測(cè)試的不通過。為規(guī)避這種問題,會(huì) Mock外部依賴:用一個(gè)不出錯(cuò)的實(shí)現(xiàn)來代替外部依賴。
Mock 第三方包的部分 api
這邊以 Mock axios[4] 為例,業(yè)務(wù)代碼:
axiosFetchUser = () => { return axios.get('/user'); }
測(cè)試代碼:
test('fetch user', () => { axios.get.mockImplementation(url => { if(/^\/user$/.test(url)) { return Promise.resolve({name: 'Joel'}) } return Promise.resolve('other') }) axiosFetchUser().then(({ name }) => { expect(name).toBe('Joel'); }) });
其中 axios.get.mockImplementation(...) Mock了 axios.get 的實(shí)現(xiàn)。
Mock 某個(gè)文件的部分內(nèi)容
我們有個(gè)工具函數(shù)文件 utils.ts,內(nèi)容如下:
const guid = () => ...; export default guid; export const getYear = () => ...; export const getMonth = () => ...;
我們只 Mock 上面文件中的 guid 和 getYear,其他部分保持原狀。這么寫:
jest.mock('./util', () => { const originalModule = jest.requireActual('./util'); return { __esModule: true, ...originalModule, default: () => 'abc', // mock guid getYear: () => 2021, }; });
Mock 完整的第三方包和文件
Mock 完整的第三方包和文件只需做 2 步。
在 __mocks__/ 下建 Mock 的文件。
通知 Jest 用 Mock 的實(shí)現(xiàn):jest.mock('./moduleName') 或 jest.mock('module_name')。
詳細(xì)介紹見: 這里[5]。
最后
行動(dòng)起來,開始練習(xí)寫單元測(cè)試吧~
參考資料
[1]Jest: https://jestjs.io/
[2]Node.js: https://nodejs.org/en/
[3]這里: https://jestjs.io/zh-Hans/docs/expect
[4]axios: https://axios-http.com/
[5]這里: https://jestjs.io/zh-Hans/docs/manual-mocks