估計(jì)很多前端都沒學(xué)過單元測試~
大廠技術(shù) 高級(jí)前端 Node進(jìn)階
點(diǎn)擊上方 程序員成長指北,關(guān)注公眾號(hào)
回復(fù)1,加入高級(jí)Node交流群
前言
對(duì)于現(xiàn)在的前端工程,一個(gè)標(biāo)準(zhǔn)完整的項(xiàng)目,通常情況單元測試是非常必要的。但很多時(shí)候我們只是完成了項(xiàng)目而忽略了項(xiàng)目測試。我認(rèn)為其中一個(gè)很大的原因是很多人對(duì)單元測試認(rèn)知不夠,因此我寫了這邊文章,一方面期望通過這篇文章讓你對(duì)單元測試有一個(gè)初步認(rèn)識(shí)。另一個(gè)方面希望通過代碼示例,讓你掌握寫單元測試實(shí)踐能力。
前端為什么需要單元測試?
-
必要性:JavaScript 缺少類型檢查,編譯期間無法定位到錯(cuò)誤,單元測試可以幫助你測試多種異常情況。
-
正確性:測試可以驗(yàn)證代碼的正確性,在上線前做到心里有底。
-
自動(dòng)化:通過 console 雖然可以打印出內(nèi)部信息,但是這是一次性的事情,下次測試還需要從頭來過,效率不能得到保證。通過編寫測試用例,可以做到一次編寫,多次運(yùn)行。
-
保證重構(gòu):互聯(lián)網(wǎng)行業(yè)產(chǎn)品迭代速度很快,迭代后必然存在代碼重構(gòu)的過程,那怎么才能保證重構(gòu)后代碼的質(zhì)量呢?有測試用例做后盾,就可以大膽的進(jìn)行重構(gòu)。
現(xiàn)狀
下面是一份抽樣調(diào)查片段,抽樣依據(jù)如下:
-
向 200 名相關(guān)者發(fā)出在線問卷調(diào)查,其中 70 人回答了問卷中的問題,前端人數(shù)占 81.16%,如果你有興趣的話,也可以幫我填一下調(diào)查問卷 (https://www.wjx.cn/vm/Ombu9q1.aspx)
-
數(shù)據(jù)收集日期:2021.09.21—2021.10.08
-
目標(biāo)群體:所有開發(fā)人員
-
組織規(guī)模:不到 50 人,50 到 100人, 100人以上
你執(zhí)行過 JavaScript 單元測試嗎?
調(diào)查中的另一個(gè)有趣的見解是,在大型組織中單元測試更受歡迎。其中一個(gè)原因可能是,由于大型組織需要處理大規(guī)模的產(chǎn)品,以及頻繁的功能迭代吧。這種持續(xù)的迭代方式,迫使他們進(jìn)行自動(dòng)化測試的投入。更具體地說,單元測試有助于增強(qiáng)產(chǎn)品的整體質(zhì)量。
另外,報(bào)告顯示超 80% 人認(rèn)為單元測試可以有效的提高質(zhì)量,超 60% 人使用過 Jest 去編寫前端單元測試,超 40% 的人認(rèn)為單元測試覆蓋率是重要的且覆蓋率應(yīng)該大于 80%。
常見單元測試工具
目前用的最多的前端單元測試框架主要有 Mocha (https://mochajs.cn/)、Jest (https://www.jestjs.cn/),但我推薦你使用 Jest,因?yàn)?Jest 和 Mocha 相比,無論從 github starts & issues 量,npm下載量相比,都有明顯優(yōu)勢。
github stars 以及 npm 下載量的實(shí)時(shí)數(shù)據(jù),參見:jest vs mocha (https://www.npmtrends.com/jest-vs-mocha) 截圖日期為 2021.11.25
Github stars & issues
npm 下載量
Jest 的下載量較大,一部分原因是因?yàn)?create-react-app 腳手架默認(rèn)內(nèi)置了 Jest, 而大部分 react 項(xiàng)目都是用它生成的。
從 github starts & issues 以及 npm 下載量角度來看,Jest 的關(guān)注度更高,社區(qū)也更活躍
框架對(duì)比
| 框架 | 斷言 | 異步 | 代碼覆蓋率 |
|---|---|---|---|
| Mocha | 不支持(需要其他庫支持) | 友好 | 不支持(需要其他庫支持) |
| Jest | 默認(rèn)支持 | 友好 | 支持 |
-
Mocha 生態(tài)好,但是需要較多的配置來實(shí)現(xiàn)高擴(kuò)展性 -
Jest 開箱即用
比如對(duì) sum 函數(shù)寫用例
./sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
Mocha + Chai 方式
Mocha 需要引入 chai 或則其他斷言庫去斷言, 如果你需要查看覆蓋率報(bào)告你還需要安裝 nyc 或者其他覆蓋率工具
./test/sum.test.js
const { expect, assert } = require('chai');
const sum = require('../sum');
describe('sum', function() {
it('adds 1 + 2 to equal 3', () => {
assert(sum(1, 2) === 3);
});
});
Jest 方式
Jest 默認(rèn)支持?jǐn)嘌?,同時(shí)默認(rèn)支持覆蓋率測試
./test/sum.test.js
const sum = require('./sum');
describe('sum function test', () => {
it('sum(1, 2) === 3', () => {
expect(sum(1, 2)).toBe(3);
});
// 這里 test 和 it 沒有明顯區(qū)別,it 是指: it should xxx, test 是指 test xxx
test('sum(1, 2) === 3', () => {
expect(sum(1, 2)).toBe(3);
});
})
可見無論是受歡迎度和寫法上,Jest 都有很大的優(yōu)勢,因此推薦你使用開箱即用的 Jest
如何開始?
1.安裝依賴
npm install --save-dev jest
2.簡單的例子
首先,創(chuàng)建一個(gè) sum.js 文件
./sum.js
function sum(a, b) {
return a + b;
}
module.exports = sum;
創(chuàng)建一個(gè)名為 sum.test.js 的文件,這個(gè)文件包含了實(shí)際測試內(nèi)容:
./test/sum.test.js
const sum = require('../sum');
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
將下面的配置部分添加到你的 package.json 里面
{
"scripts": {
"test": "jest"
},
}
運(yùn)行 npm run test ,jest 將打印下面這個(gè)消息
3.不支持部分 ES6 語法
nodejs 采用的是 CommonJS 的模塊化規(guī)范,使用 require 引入模塊;而 import 是 ES6 的模塊化規(guī)范關(guān)鍵字。想要使用 import,必須引入 babel 轉(zhuǎn)義支持,通過 babel 進(jìn)行編譯,使其變成 node 的模塊化代碼
如以下文件改寫成 ES6 寫法后,運(yùn)行 npm run test將會(huì)報(bào)錯(cuò)
./sum.js
export function sum(a, b) {
return a + b;
}
./test/sum.test.js
import { sum } from '../sum';
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
報(bào)錯(cuò)
為了能使用這些新特性,我們就需要使用 babel 把 ES6 轉(zhuǎn)成 ES5 語法
解決辦法
安裝依賴
npm install --save-dev @babel/core @babel/preset-env
根目錄加入.babelrc
{ "presets": ["@babel/preset-env"] }
再次運(yùn)行 npm run test ,問題解決
原理
jest 運(yùn)行時(shí)內(nèi)部先執(zhí)行( jest-babel ),檢測是否安裝 babel-core,然后取 .babelrc 中的配置運(yùn)行測試之前結(jié)合 babel 先把測試用例代碼轉(zhuǎn)換一遍然后再進(jìn)行測試
4.測試 ts 文件
jest 需要借助 .babelrc 去解析 TypeScript 文件再進(jìn)行測試
安裝依賴
npm install --save-dev @babel/preset-typescript
**改寫 **.babelrc
{ "presets": ["@babel/preset-env", "@babel/preset-typescript"] }
為了解決編輯器對(duì) jest 斷言方法的類型報(bào)錯(cuò),如 test、expect 的報(bào)錯(cuò),你還需要安裝
npm install --save-dev @types/jest
./get.ts
/**
* 訪問嵌套對(duì)象,避免代碼中出現(xiàn)類似 user && user.personalInfo ? user.personalInfo.name : null 的代碼
*/
export function get<T>(object: any, path: Array<number | string>, defaultValue?: T) : T {
const result = path.reduce((obj, key) => obj !== undefined ? obj[key] : undefined, object);
return result !== undefined ? result : defaultValue;
}
./test/get.test.ts
import { get } from './get';
test('測試嵌套對(duì)象存在的可枚舉屬性 line1', () => {
expect(get({
id: 101,
email: '[email protected]',
personalInfo: {
name: 'Jack',
address: {
line1: 'westwish st',
line2: 'washmasher',
city: 'wallas',
state: 'WX'
}
}
}, ['personalInfo', 'address', 'line1'])).toBe('westwish st');
});
運(yùn)行 npm run test
5.持續(xù)監(jiān)聽
為了提高效率,可以通過加啟動(dòng)參數(shù)的方式讓 jest 持續(xù)監(jiān)聽文件的修改,而不需要每次修改完再重新執(zhí)行測試用例
改寫 package.json
"scripts": { "test": "jest --watchAll" },
效果
5.生成測試覆蓋率報(bào)告
什么是單元測試覆蓋率?
單元測試覆蓋率是一種軟件測試的度量指標(biāo),指在所有功能代碼中,完成了單元測試的代碼所占的比例。有很多自動(dòng)化測試框架工具可以提供這一統(tǒng)計(jì)數(shù)據(jù),其中最基礎(chǔ)的計(jì)算方式為:
單元測試覆蓋率 = 被測代碼行數(shù) / 參測代碼總行數(shù) * 100%
如何生成?
加入 jest.config.js 文件
module.exports = {
// 是否顯示覆蓋率報(bào)告
collectCoverage: true,
// 告訴 jest 哪些文件需要經(jīng)過單元測試測試
collectCoverageFrom: ['get.ts', 'sum.ts', 'src/utils/**/*'],
}
再次運(yùn)行效果
參數(shù)解讀
| 參數(shù)名 | 含義 | 說明 |
|---|---|---|
| % stmts | 語句覆蓋率 | 是不是每個(gè)語句都執(zhí)行了? |
| % Branch | 分支覆蓋率 | 是不是每個(gè) if 代碼塊都執(zhí)行了? |
| % Funcs | 函數(shù)覆蓋率 | 是不是每個(gè)函數(shù)都調(diào)用了? |
| % Lines | 行覆蓋率 | 是不是每一行都執(zhí)行了? |
設(shè)置單元測試覆蓋率閥值
個(gè)人認(rèn)為既然在項(xiàng)目中集成了單元測試,那么非常有必要關(guān)注單元測試的質(zhì)量,而覆蓋率則一定程度上客觀的反映了單測的質(zhì)量,同時(shí)我們還可以通過設(shè)置單元測試閥值的方式提示用戶是否達(dá)到了預(yù)期質(zhì)量。
jest.config.js 文件
module.exports = {
collectCoverage: true, // 是否顯示覆蓋率報(bào)告
collectCoverageFrom: ['get.ts', 'sum.ts', 'src/utils/**/*'], // 告訴 jest 哪些文件需要經(jīng)過單元測試測試
coverageThreshold: {
global: {
statements: 90, // 保證每個(gè)語句都執(zhí)行了
functions: 90, // 保證每個(gè)函數(shù)都調(diào)用了
branches: 90, // 保證每個(gè) if 等分支代碼都執(zhí)行了
},
},
上述閥值要求我們的測試用例足夠充分,如果我們的用例沒有足夠充分,則下面的報(bào)錯(cuò)將會(huì)幫助你去完善
6.如何編寫單元測試
下面我們以 fetchEnv 方法作為案例,編寫一套完整的單元測試用例供讀者參考
編寫 fetchEnv 方法
./src/utils/fetchEnv.ts 文件
/**
* 環(huán)境參數(shù)枚舉
*/
enum IEnvEnum {
DEV = 'dev', // 開發(fā)
TEST = 'test', // 測試
PRE = 'pre', // 預(yù)發(fā)
PROD = 'prod', // 生產(chǎn)
}
/**
* 根據(jù)鏈接獲取當(dāng)前環(huán)境參數(shù)
* @param {string?} url 資源鏈接
* @returns {IEnvEnum} 環(huán)境參數(shù)
*/
export function fetchEnv(url: string): IEnvEnum {
const envs = [IEnvEnum.DEV, IEnvEnum.TEST, IEnvEnum.PRE];
return envs.find((env) => url.includes(env)) || IEnvEnum.PROD;
}
編寫對(duì)應(yīng)的單元測試
./test/fetchEnv.test.ts 文件
import { fetchEnv } from '../src/utils/fetchEnv';
describe('fetchEnv', () => {
it ('判斷是否 dev 環(huán)境', () => {
expect(fetchEnv('https://www.imooc.dev.com/')).toBe('dev');
});
it ('判斷是否 test 環(huán)境', () => {
expect(fetchEnv('https://www.imooc.test.com/')).toBe('test');
});
it ('判斷是否 pre 環(huán)境', () => {
expect(fetchEnv('https://www.imooc.pre.com/')).toBe('pre');
});
it ('判斷是否 prod 環(huán)境', () => {
expect(fetchEnv('https://www.imooc.prod.com/')).toBe('prod');
});
it ('判斷是否 prod 環(huán)境', () => {
expect(fetchEnv('https://www.imooc.com/')).toBe('prod');
});
});
執(zhí)行結(jié)果
7.常用斷言方法
關(guān)于斷言方法有很多,這里僅摘出常用方法,如果你想了解更多,你可以去 Jest 官網(wǎng) API (https://www.jestjs.cn/docs/expect) 部分查看
.not 修飾符允許你測試結(jié)果不等于某個(gè)值的情況
./test/sum.test.js
import { sum } from './sum';
test('sum(2, 4) 不等于 5', () => {
expect(sum(2, 4)).not.toBe(5);
})
.toEqual 匹配器會(huì)遞歸的檢查對(duì)象所有屬性和屬性值是否相等,常用來檢測引用類型
./src/utils/userInfo.js
export const getUserInfo = () => {
return {
name: 'moji',
age: 24,
}
}
./test/userInfo.test.js
import { getUserInfo } from '../src/userInfo.js';
test('getUserInfo()返回的對(duì)象深度相等', () => {
expect(getUserInfo()).toEqual(getUserInfo());
})
test('getUserInfo()返回的對(duì)象內(nèi)存地址不同', () => {
expect(getUserInfo()).not.toBe(getUserInfo());
})
.toHaveLength 可以很方便的用來測試字符串和數(shù)組類型的長度是否滿足預(yù)期
./src/utils/getIntArray.js
export const getIntArray = (num) => {
if (!Number.isInteger(num)) {
throw Error('"getIntArray"只接受整數(shù)類型的參數(shù)');
}
return [...new Array(num).keys()];
};
./test/getIntArray.test.js
./test/getIntArray.test.js
import { getIntArray } from '../src/utils/getIntArray';
test('getIntArray(3)返回的數(shù)組長度應(yīng)該為3', () => {
expect(getIntArray(3)).toHaveLength(3);
})
.toThorw 能夠讓我們測試被測試方法是否按照預(yù)期拋出異常
但是需要注意的是:我們必須使用一個(gè)函數(shù)將被測試的函數(shù)做一個(gè)包裝,正如下面 getIntArrayWrapFn 所做的那樣,否則會(huì)因?yàn)楹瘮?shù)拋出錯(cuò)誤導(dǎo)致該斷言失敗。
./test/getIntArray.test.js
import { getIntArray } from '../src/utils/getIntArray';
test('getIntArray(3.3)應(yīng)該拋出錯(cuò)誤', () => {
function getIntArrayWrapFn() {
getIntArray(3.3);
}
expect(getIntArrayWrapFn).toThrow('"getIntArray"只接受整數(shù)類型的參數(shù)');
})
.toMatch 傳入一個(gè)正則表達(dá)式,它允許我們來進(jìn)行字符串類型的正則匹配
./test/userInfo.test.js
import { getUserInfo } from '../src/utils/userInfo.js';
test("getUserInfo().name 應(yīng)該包含'mo'", () => {
expect(getUserInfo().name).toMatch(/mo/i);
})
測試異步函數(shù)
./servers/fetchUser.js
/**
* 獲取用戶信息
*/
export const fetchUser = () => {
return new Promise((resole) => {
setTimeout(() => {
resole({
name: 'moji',
age: 24,
})
}, 2000)
})
}
./test/fetchUser.test.js
import { fetchUser } from '../src/fetchUser';
test('fetchUser() 可以請(qǐng)求到一個(gè)用戶名字為 moji', async () => {
const data = await fetchUser();
expect(data.name).toBe('moji')
})
這里你可能看到這樣一條報(bào)錯(cuò)
這是因?yàn)?nbsp;@babel/preset-env 不支持 async await 導(dǎo)致的,這時(shí)候就需要對(duì) babel 配置進(jìn)行增強(qiáng),可以安裝 @babel/plugin-transform-runtime 這個(gè)插件解決
npm install --save-dev @babel/plugin-transform-runtime
同時(shí)改寫 .babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"plugins": ["@babel/plugin-transform-runtime"]
}
再次運(yùn)行就不會(huì)出現(xiàn)報(bào)錯(cuò)了
.toContain 匹配對(duì)象中是否包含
./test/toContain.test.js
const names = ['liam', 'jim', 'bart'];
test('匹配對(duì)象是否包含', () => {
expect(names).toContain('jim');
})
檢查一些特殊的值(null,undefined 和 boolean)
toBeNull 僅匹配 null
toBeUndefined 僅匹配 undefined
toBeDefined 與…相反 toBeUndefined
toBeTruthy 匹配 if 語句視為 true 的任何內(nèi)容
toBeFalsy 匹配 if 語句視為 false 的任何內(nèi)容
檢查數(shù)字類型(number)
toBeGreaterThan 大于
toBeGreaterThanOrEqual 至少(大于等于)
toBeLessThan 小于
toBeLessThanOrEqual 最多(小于等于)
toBeCloseTo 用來匹配浮點(diǎn)數(shù)(帶小數(shù)點(diǎn)的相等)
總結(jié)
以上就是文章全部內(nèi)容,相信你閱讀完這篇文章后,已經(jīng)掌握了前端單元測試的基本知識(shí),甚至可以按照文章教學(xué)步驟,現(xiàn)在就可以在你的項(xiàng)目中接入單元測試。同時(shí)在閱讀過程中如果你有任何問題,或者有更好見解,更好的框架推薦,歡迎你在評(píng)論區(qū)留言!
也許在你閱讀這篇文章之前,你本身就已掌握前端單元測試技能了,甚至已經(jīng)是這個(gè)領(lǐng)域的大牛了,那么首先我感到非常榮幸,同時(shí)也誠懇的邀請(qǐng)你在評(píng)論區(qū)提出寶貴意見,我在這里提前說聲謝謝!
最后感謝你在百忙之中抽出時(shí)間閱讀這篇文章,送人玫瑰,手有余香,如果你覺得文章對(duì)你有所幫助,希望可以幫我點(diǎn)個(gè)贊!
我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。
“分享、點(diǎn)贊、在看” 支持一波??
