前端自動(dòng)化測(cè)試 之 Jest 測(cè)試框架應(yīng)用
前端自動(dòng)化測(cè)試 —— Jest 測(cè)試框架應(yīng)用
http://zoo.zhengcaiyun.cn/blog/article/jest
什么是自動(dòng)化測(cè)試
在軟件測(cè)試中,自動(dòng)化測(cè)試指的是使用獨(dú)立于待測(cè)軟件的其他軟件來(lái)自動(dòng)執(zhí)行測(cè)試、比較實(shí)際結(jié)果與預(yù)期并生成測(cè)試報(bào)告這一過(guò)程。在測(cè)試流程已經(jīng)確定后,測(cè)試自動(dòng)化可以自動(dòng)執(zhí)行的一些重復(fù)但必要的測(cè)試工作。也可以完成手動(dòng)測(cè)試幾乎不可能完成的測(cè)試。對(duì)于持續(xù)交付和持續(xù)集成的開(kāi)發(fā)方式而言,測(cè)試自動(dòng)化是至關(guān)重要的。 ——來(lái)自 WiKi 百科
為什么要用前端自動(dòng)化測(cè)試
隨著前端項(xiàng)目的發(fā)展,其規(guī)模和功能日益增加。為了提高項(xiàng)目的穩(wěn)定性和可靠性,除了需要測(cè)試工程師外,前端自動(dòng)化測(cè)試也成為了不可或缺的一環(huán)。采用前端自動(dòng)化測(cè)試可以有效地提高代碼質(zhì)量,降低出錯(cuò)的概率,從而使項(xiàng)目更加健壯和更易維護(hù)。
前端自動(dòng)化分類(lèi)和思想
單元測(cè)試
又稱(chēng)為模塊測(cè)試 ,是針對(duì)程序模塊(軟件設(shè)計(jì)的最小單位)來(lái)進(jìn)行正確性檢驗(yàn)的測(cè)試工作。在前端中,一個(gè)函數(shù)、一個(gè)類(lèi)、一個(gè)模塊文件,都可以進(jìn)行單元測(cè)試,測(cè)試時(shí)每個(gè)模塊都是互不干擾的。
集成測(cè)試
是在單元測(cè)試的基礎(chǔ)上,測(cè)試再將所有的軟件單元按照概要設(shè)計(jì)規(guī)格說(shuō)明的要求組裝成模塊、子系統(tǒng)或系統(tǒng)的過(guò)程中各部分工作是否達(dá)到或?qū)崿F(xiàn)相應(yīng)技術(shù)指標(biāo)及要求的活動(dòng)。用戶(hù)的開(kāi)始操作到結(jié)束操作這一整個(gè)行為流程可以當(dāng)作集成測(cè)試。
TDD 測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test Driven Development)
開(kāi)發(fā)流程:
TDD 是趨向于白盒測(cè)試,需要開(kāi)發(fā)者對(duì)當(dāng)前編寫(xiě)的模塊思路足夠清晰。
優(yōu)勢(shì):
- 長(zhǎng)期減少回歸 bug。
- 代碼質(zhì)量更好,可維護(hù)性高。
- 測(cè)試覆蓋率高(先寫(xiě)測(cè)試用例,再實(shí)現(xiàn)功能)。
- 錯(cuò)誤測(cè)試代碼不容易出現(xiàn)(測(cè)試在開(kāi)發(fā)之前執(zhí)行)。
BDD 行為驅(qū)動(dòng)開(kāi)發(fā)(Behavior Driven Development)
開(kāi)發(fā)流程:
BDD 趨向于黑盒測(cè)試,只關(guān)注用戶(hù)的一整套行為流程下來(lái)是否會(huì)成功。
優(yōu)勢(shì):
- 對(duì)于用戶(hù)行為的整個(gè)流程把控程度較高,對(duì)于開(kāi)發(fā)人員來(lái)說(shuō)這樣安全感高。
如何自己寫(xiě)非框架測(cè)試用例
不使用測(cè)試框架,我們?cè)撊绾螠y(cè)試自己的模塊呢?如果我們想要測(cè)試下面的代碼,應(yīng)該需要兩個(gè)值,一個(gè)是 期望值 ,另一個(gè)是函數(shù)執(zhí)行的 結(jié)果值 ,我們需要對(duì)比兩個(gè)值來(lái)進(jìn)行判斷當(dāng)前函數(shù)是否通過(guò)了測(cè)試用例。
// index.js
function ZcyZooTeam(str) {
return 'Zcy' + str;
}
需要下面的 if / else 進(jìn)行判斷當(dāng)前的期望值 value 和結(jié)果值 result 是否相等,如果相等說(shuō)明我們的測(cè)試用例通過(guò)了。我們將這兩段代碼復(fù)制到瀏覽器中,下面的執(zhí)行不會(huì)通過(guò),并會(huì)拋出錯(cuò)誤,只有我們將傳入值改為 ZooTeam 才會(huì)成功執(zhí)行。
// no-jest.js
const result = ZcyZooTeam('Zero');
const value = 'ZooTeam';
if(result !== value) {
throw Error(`ZcyZooTeam 結(jié)果應(yīng)為${value}, 但實(shí)際結(jié)果為${result}`);
}
是否能簡(jiǎn)化?
如果我們有多個(gè)函數(shù)需要測(cè)試,你應(yīng)該不想寫(xiě)許多個(gè) if / else 代碼塊吧?所以我們要將上面的代碼塊進(jìn)行優(yōu)化成一個(gè)函數(shù)。
// no-jest.js
function expect(result) {
return {
// 用于判斷是否為期望值
toBe(value) {
if(result !== value) {
throw Error(`結(jié)果應(yīng)為${value}, 但實(shí)際結(jié)果為${result}`);
}
console.log('測(cè)試通過(guò)!');
}
}
}
// 執(zhí)行測(cè)試
expect(ZcyZooTeam('Zero')).toBe('ZcyZooTeam');
經(jīng)過(guò)上面的封裝,我們就可以只寫(xiě)一行代碼進(jìn)行測(cè)試了!
如何能清晰地看到我測(cè)的是哪個(gè)呢?
雖然上面的封裝只需要書(shū)寫(xiě)一行代碼就可以測(cè)試了,但是我們不知道執(zhí)行結(jié)果和測(cè)試用例之間的對(duì)應(yīng)關(guān)系,我們需要輸出的文字來(lái)告訴我們當(dāng)前是哪個(gè)測(cè)試用例執(zhí)行了。
// no-jest.js
// 再封裝如下方法
function test(msg, fn) {
try {
fn();
console.log(msg + '測(cè)試通過(guò)!');
} catch (error) {
console.log(msg + '測(cè)試未通過(guò)!' + error);
}
}
test('測(cè)試ZcyZooTeam', () => {
expect(ZcyZooTeam('Zero')).toBe('ZcyZooTeam')
})
成功和失敗都會(huì)進(jìn)行提示,這樣我們就可以知道當(dāng)前是哪個(gè)測(cè)試用例成功/失敗了。
Jest 的書(shū)寫(xiě)方式也是同上,如果上面的一整套代碼了解了的話,你已經(jīng)可以寫(xiě) Jest 的測(cè)試腳本了,下面將進(jìn)入 Jest 的配置。
如何使用 Jest 測(cè)試框架進(jìn)行自動(dòng)化測(cè)試?
主流的前端自動(dòng)化測(cè)試框架
Jasmine
Jasmine 優(yōu)點(diǎn):易于學(xué)習(xí)和使用,支持異步測(cè)試,可以在瀏覽器和 Node.js 環(huán)境中運(yùn)行,可以生成易于閱讀的測(cè)試報(bào)告,可以與其他庫(kù)和框架集成。
MOCHA
MOCHA 優(yōu)點(diǎn):支持異步測(cè)試和 Promise ,可以在瀏覽器和 Node.js 環(huán)境中運(yùn)行,可以與其他庫(kù)和框架集成,可以生成易于閱讀的測(cè)試報(bào)告,可以使用各種插件和擴(kuò)展來(lái)增強(qiáng)其功能。
Jest
Jest 是針對(duì)模塊進(jìn)行測(cè)試,單元測(cè)試對(duì)單個(gè)模塊進(jìn)行測(cè)試,集成測(cè)試對(duì)多個(gè)模塊進(jìn)行測(cè)試。
Jest 優(yōu)點(diǎn):速度快(單模塊測(cè)試時(shí),執(zhí)行過(guò)的模塊不會(huì)重復(fù)執(zhí)行),API簡(jiǎn)單,易配置,隔離性好(執(zhí)行環(huán)境相對(duì)隔離,每個(gè)文件單獨(dú)隔離互不干擾),監(jiān)控模式(更靈活的運(yùn)行各種測(cè)試用例),適配編輯器多,Snapshot(快照),多項(xiàng)目運(yùn)行(后臺(tái)前臺(tái)測(cè)試用例并行測(cè)試),生成可視化覆蓋率簡(jiǎn)單,Mock 豐富。
準(zhǔn)備工作 —— Jest 的配置
npm i jest --save-D
// 初始化 jest 的配置文件
npx jest --init
// 你將在那個(gè)環(huán)境進(jìn)行測(cè)試,回車(chē)即可選擇
// 第一個(gè)是 node 環(huán)境、第二個(gè)是瀏覽器環(huán)境
? Choose the test environment that will be used for testing ? - Use arrow-keys. Return to submit.
node
? jsdom (browser-like)
// 是否需要 jest 生成測(cè)試覆蓋率報(bào)告
? Do you want Jest to add coverage reports? ? (y/N)
// 是否需要在測(cè)試結(jié)束后清除模擬調(diào)用
? Automatically clear mock calls and instances between every test? ? (y/N)
// 創(chuàng)建 jest.config.js 文件
?? Configuration file created at /Users/zcy1/Desktop/demo/auto-test-jest-demo/jest.config.js
以上方法執(zhí)行結(jié)束后,會(huì)生成一個(gè) jest.config.js 文件,里面包含了 Jest 的配置項(xiàng),每個(gè)配置項(xiàng)都會(huì)帶有描述,在初始化的兩個(gè)配置也會(huì)體現(xiàn)在配置文件中。
使用 babel 轉(zhuǎn)換來(lái)使用 ES6 形式的導(dǎo)入和導(dǎo)出
// .babelrc
// 如果想用 es6 的形式導(dǎo)出,需要使用 babel 插件進(jìn)行轉(zhuǎn)換
// @babel/core @babel/preset-env
// 創(chuàng)建 .babelrc 文件
// 為了在 node 環(huán)境下使用 es6 的導(dǎo)出,需要使用 babel 進(jìn)行轉(zhuǎn)換
{
// 設(shè)置插件集合
"presets": [
// 使用當(dāng)前插件,可以進(jìn)行轉(zhuǎn)換
// 數(shù)組的第二項(xiàng)為插件的配置項(xiàng)
[
"@babel/preset-env", {
// 根據(jù) node 的版本號(hào)來(lái)結(jié)合插件對(duì)代碼進(jìn)行轉(zhuǎn)換
"targets": {
"node": "current"
}
}
]
]
}
配置好后需要將 package.json 中的 test 命令的 value 改為 jest --watchAll ,代表監(jiān)聽(tīng)所有有修改的測(cè)試文件,然后控制臺(tái)執(zhí)行 npm run test 就可以執(zhí)行測(cè)試用例了。
Jest 啟動(dòng)時(shí)會(huì)進(jìn)行如下流程
- npm run test
- jest (babel-jest) 檢測(cè)當(dāng)前環(huán)境是否安裝了 babel
- 如果安裝了則會(huì)去 babelrc 中取配置
- 取到后執(zhí)行代碼轉(zhuǎn)換
- 最后再執(zhí)行轉(zhuǎn)化過(guò)的測(cè)試用例代碼
如何生成一個(gè)測(cè)試用例覆蓋率報(bào)告?
經(jīng)過(guò)上面的 Jest 配置,我們就可以通過(guò)下面的 npx 命令來(lái)生成測(cè)試覆蓋率報(bào)告了。
npx jest --coverage
會(huì)生成一個(gè)名為 coverage 的文件夾,打開(kāi)里面的 html 就可以看到你的覆蓋率,其中 Statements 是語(yǔ)句覆蓋率(每個(gè)語(yǔ)句是否執(zhí)行),Branches 是分支覆蓋率(每個(gè) if 塊是否執(zhí)行),F(xiàn)unctions是函數(shù)覆蓋率(每個(gè)函數(shù)是否執(zhí)行),Lines 是行覆蓋率(每行是否執(zhí)行),通過(guò)修改 coverageDirectory 的值可以改變測(cè)試覆蓋率生成文件夾的名字。
Jest 基礎(chǔ)匹配器
上面我們說(shuō)過(guò)了,Jest 的用法和我們封裝的那幾個(gè)函數(shù)是一樣的,都是執(zhí)行 test 函數(shù)并向函數(shù)中傳遞參數(shù),第一個(gè)參數(shù)是你當(dāng)前測(cè)試用例的描述,第二個(gè)參數(shù)是需要執(zhí)行的匹配規(guī)則。
匹配器
toBe
toBe 匹配器,期待是否與匹配器中的值相等 相當(dāng)于 object.is ===
// jest.test.js
test("測(cè)試", () => {
expect(1).toBe(1); // 通過(guò)
const a = { name: 'Zero' };
// 因?yàn)?a 的引用地址,和 toBe 中對(duì)象的引用地址不一致,會(huì)導(dǎo)致測(cè)試不通過(guò),需要使用其他的匹配器
expect(a).toBe({ name: 'Zero' }); // 失敗
});
toEqual
toEqual 匹配器,只會(huì)匹配對(duì)象中的內(nèi)容是否相等。
// jest.test.js
test('測(cè)試對(duì)象相等', () => {
const a = { name: 'Zero' };
expect(a).toEqual({ name: 'Zero' }); // 斷言
})
toBeNull
toBeNull 匹配器,可以判斷變量是否為 null ,只能匹配 null。
// jest.test.js
test('測(cè)試是否為null', () => {
const a = null;
expect(a).toBeNull();
})
toBeUndefined
toBeUndefined 匹配器,可以判斷變量是否為 undefined ,只能匹配 undefined。
// jest.test.js
test('測(cè)試是否為undefined', () => {
const a = undefined;
expect(a).toBeUndefined();
})
toBeDefined
toBeDefined 匹配器,希望被測(cè)試的值是定義好的。
// jest.test.js
test('測(cè)試變量是否定義過(guò)', () => {
const a = '';
expect(a).toBeDefined();
})
toBeTruthy
toBeTruthy 匹配器,可以判斷變量是否為真值,會(huì)對(duì)非 bool 值進(jìn)行轉(zhuǎn)換。
// jest.test.js
test('測(cè)試變量真值', () => {
const a = '123';
expect(a).toBeTruthy();
})
toBeFalsy
toBeFalsy 匹配器,可以判斷變量是否為假值,會(huì)對(duì)非 bool 值進(jìn)行轉(zhuǎn)換。
// jest.test.js
test('測(cè)試變量假值', () => {
const a = '';
expect(a).toBeFalsy();
})
not修飾符
not 匹配器,可以將匹配后的結(jié)果進(jìn)行取反。
// jest.test.js
test('測(cè)試變量不是假值', () => {
const a = '1';
expect(a).not.toBeFalsy();
})
toBeGreaterThan
toBeGreaterThan 匹配器,期望值是否大于匹配器的參數(shù)。
// jest.test.js
test('是否大于 a 的數(shù)字', () => {
const a = 123;
expect(a).toBeGreaterThan(1);
})
toBeLessThan
toBeLessThan 匹配器,期望值是否小于匹配器的參數(shù)。
// jest.test.js
test('是否小于 a 的數(shù)字', () => {
const a = 0;
expect(a).toBeLessThan(1);
})
toBeGreaterThanOrEqual
toBeGreaterThanOrEqual 匹配器,期望值是否大于或等于匹配器的參數(shù)。
// jest.test.js
test('是否大于等于 a 的數(shù)字', () => {
// toBeLessOrEqual 匹配器,與之相反
const a = 123;
expect(a).toBeGreaterThanOrEqual(1);
})
toBeCloseTo
js 中,浮點(diǎn)數(shù)值在相加時(shí)不準(zhǔn)確,使用 toBeCloseTo 匹配器解決,趨近于 0.3。
// jest.test.js
test('是否大于等于 a 的數(shù)字', () => {
const a1 = 0.1;
const a2 = 0.2;
expect(a1 + a2).toBeCloseTo(0.3);
})
toMatch
toMatch 匹配器,匹配當(dāng)前字符串中是否含有這個(gè)值,支持正則。
// jest.test.js
test('是否包含 day ', () => {
const a = 'happy every day';
expect(a).toMatch('day');
})
toContain
toContain 匹配器,判斷當(dāng)前數(shù)組中是否包含這個(gè)元素,Set 也可以使用。
// jest.test.js
test('數(shù)組中是否包含 zoo 這個(gè)元素', () => {
const a = ['zoo', 'ZooTeam', 'Zero'];
expect(a).toContain('zoo');
})
toThrow
toThrow 匹配器,可以捕捉拋出的異常,參數(shù)為拋出的 error ,可以用來(lái)判斷是否為某個(gè)異常。
// jest.test.js
const error = () => {
throw new Error('error');
}
test('是否存在異常', () => {
expect(error).toThrow();
})
以上就是 Jest 中比較基礎(chǔ)的匹配器,可以結(jié)合 初始化 + 配置 + 基礎(chǔ)匹配器 進(jìn)行書(shū)寫(xiě)測(cè)試用例。
命令行操作
在運(yùn)行 npm run test 命令的時(shí)候,控制臺(tái)執(zhí)行測(cè)試用例成功或失敗后都會(huì)像下面的圖片一樣出現(xiàn)幾行提示,讓你按對(duì)應(yīng)的鍵進(jìn)行操作。
上面幾個(gè)命令行的意思如下:
1. f 只會(huì)跑測(cè)試未通過(guò)的用例,再次點(diǎn)擊 f 會(huì)取消當(dāng)前模式。
我們使用一個(gè)失敗的測(cè)試用例做一下示范。
按下 f 后,Jest 只會(huì)執(zhí)行剛才失敗的測(cè)試用例。
2. 只監(jiān)聽(tīng)已改變的文件,如果存在多個(gè)測(cè)試文件,可以開(kāi)啟,會(huì)與當(dāng)前 git 倉(cāng)庫(kù)中的提交進(jìn)行比較,需要使用 git 來(lái)監(jiān)聽(tīng)哪個(gè)文件修改了,也可以將 --watchAll 改為 --watch 只會(huì)運(yùn)行修改的文件。
3. 根據(jù)測(cè)試用例文件的正則表達(dá)式,過(guò)濾需要執(zhí)行的測(cè)試用例文件,No tests found, exiting with code 0 如果填寫(xiě)不對(duì)會(huì)進(jìn)行提示,并不會(huì)跑任何測(cè)試用例。
4. 根據(jù)測(cè)試用例描述的正則表達(dá)式,過(guò)濾需要執(zhí)行的測(cè)試用例。5. 退出測(cè)試用例監(jiān)聽(tīng)。
異步測(cè)試
在正常的業(yè)務(wù)開(kāi)發(fā)中,項(xiàng)目中不只有同步代碼,還會(huì)有請(qǐng)求接口的異步代碼,異步代碼的測(cè)試與同步代碼有稍許不同,我們來(lái)看一下。
編寫(xiě)一個(gè)接口請(qǐng)求
// getData.js
export const getData = (fn) => {
axios.get('/getData').then((res) => {
fn(res.data);
})
}
對(duì)異步請(qǐng)求進(jìn)行測(cè)試
// jest.test.js
// 異步調(diào)用回調(diào)函數(shù)需要添加 done 參數(shù),是一個(gè)函數(shù)
test('getData 返回結(jié)果為 { success: true }', (done) => {
// 此處代碼無(wú)效,因?yàn)闇y(cè)試用例不會(huì)等待請(qǐng)求結(jié)束后的回調(diào),測(cè)試用例執(zhí)行完就直接結(jié)束了
// getData1((data) => {
// expect(data).toEqual({
// success: true
// })
// })
getData1((data) => {
expect(data).toEqual({
success: true
})
// 需要在結(jié)束前調(diào)用 done 函數(shù), Jest 會(huì)知道到 done 才會(huì)結(jié)束,才可以正確測(cè)試異步函數(shù)
done();
})
})
需要注意的是,如果傳入了形參 done,但是沒(méi)有使用,這個(gè)測(cè)試用例就會(huì)處于一直執(zhí)行的狀態(tài),直到執(zhí)行超時(shí)。
還可以結(jié)合 promise 進(jìn)行使用
// getData.js
export const getData2 = () => {
return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
// jest.test.js
test('getData 返回結(jié)果為 { success: true }', () => {
// 使用 promise 時(shí)需要 return,在 then 中使用 done 也可以
return getData2().then(res => {
expect(res.data).toEqual({
success: true
})
})
})
// 測(cè)試請(qǐng)求是否 404
test('getData 返回結(jié)果為 404', () => {
// 由于不觸發(fā) catch 就不會(huì)走測(cè)試校驗(yàn),所以會(huì)成功,我們需要做一下限制
// 這行代碼限制下面的代碼中必須要執(zhí)行一次 expect 方法,如果非 404 就不會(huì)走下面的 expect,則測(cè)試不會(huì)通過(guò)
expect.assertions(1);
// 使用 promise 時(shí)需要 return
// 如果只想測(cè)試 404 這樣寫(xiě)是有問(wèn)題的,需要配合 assertions 使用
return getData2().catch(err => {
expect(err.toString().indexOf('404') > -1).toBe(true)
})
})
// 另一種寫(xiě)法
test('getData 返回結(jié)果為 { success: true }', () => {
// 會(huì)返回很多數(shù)據(jù),其中包含 data 對(duì)象
// getData2().then((res) => console.log(res))
// {
// status: 200,
// statusText: 'OK',
// headers: {},
// ......
// data: { success: true }
// }
// resolves 方法會(huì)將接口返回的字段全部獲取,再使用 toMatchObject 方法進(jìn)行匹配大對(duì)象中是否存在 data 對(duì)象
return expect(getData2()).resolves.toMatchObject({
data: {
success: true
}
})
})
// 還可以使用 async/await
test('getData 返回結(jié)果為 { success: true }', async () => {
await expect(getData2()).resolves.toMatchObject({
data: {
success: true
}
})
})
鉤子函數(shù)
鉤子函數(shù)可以當(dāng)作一個(gè)測(cè)試用例的生命周期來(lái)看待,有 beforeAll 、beforeEach 、afterEach 、afterAll 。
以下是一些關(guān)于鉤子函數(shù)的概念和場(chǎng)景:
beforeAll:在所有測(cè)試用例執(zhí)行前運(yùn)行
beforeEach:在每個(gè)測(cè)試用例執(zhí)行前執(zhí)行一次
afterEach:在每個(gè)測(cè)試用例執(zhí)行后執(zhí)行一次
afterAll:在所有測(cè)試用例結(jié)束后運(yùn)行
有時(shí)候,需要測(cè)試一個(gè)類(lèi)中的多個(gè)方法,這些方法可能會(huì)反復(fù)操作同一個(gè)對(duì)象上的屬性。如果使用同一個(gè)實(shí)例,就會(huì)相互干擾,導(dǎo)致測(cè)試用例無(wú)法通過(guò)。此時(shí),需要使用不同的實(shí)例來(lái)進(jìn)行測(cè)試。
Counter 類(lèi)
// Counter.js
class Counter {
constructor() {
this.number = 0;
}
add() {
this.number += 1;
}
minus() {
this.number -= 1;
}
}
export default Counter;
我們想要測(cè)試?yán)锩娴?nbsp;add 和 minus 方法是否正確,需要實(shí)例化一個(gè)對(duì)象進(jìn)行測(cè)試。但是下面的測(cè)試用例使用的永遠(yuǎn)都是同一個(gè)實(shí)例,第二個(gè)測(cè)試用例永遠(yuǎn)都不會(huì)通過(guò)。因?yàn)閳?zhí)行了第一個(gè)測(cè)試用例,第二個(gè)測(cè)試用例的值只能是 0。
// jest.test.js
const count = new Counter();
// 使用下方兩種測(cè)試方法會(huì)互相影響,先加一后減一,結(jié)果永遠(yuǎn)是 0
test('測(cè)試加法', () => {
count.add();
expect(count.number).toBe(1);
})
test('測(cè)試減法', () => {
count.minus();
expect(count.number).toBe(-1);
})
需要使用鉤子函數(shù),在每次執(zhí)行測(cè)試用例的時(shí)候,都讓他重新實(shí)例化一個(gè)對(duì)象
// jest.test.js
let count = null;
// 類(lèi)似于生命周期
// 會(huì)在測(cè)試用例執(zhí)行前運(yùn)行
beforeAll(() => {
console.log('beforeAll')
});
// 會(huì)在每個(gè)測(cè)試用例執(zhí)行前執(zhí)行一次,這樣就會(huì)解決上面互相影響的問(wèn)題
beforeEach(() => {
console.log('beforeEach')
count = new Counter();
});
// 會(huì)在每個(gè)測(cè)試用例執(zhí)行后執(zhí)行一次
afterEach(() => {
console.log('afterEach')
});
// 會(huì)在所有測(cè)試用例結(jié)束后運(yùn)行
afterAll(() => {
console.log('afterAll');
});
test('測(cè)試加法', () => {
console.log('add')
count.add();
expect(count.number).toBe(1);
})
test('測(cè)試減法', () => {
console.log('minus')
count.minus();
expect(count.number).toBe(-1);
})
分組方法 discribe
// jest.test.js
let count = null;
// describe 方法,可以將測(cè)試用例進(jìn)行分組,更加好維護(hù)同類(lèi)型功能的測(cè)試用例
describe('count 測(cè)試', () => {
beforeAll(() => {
console.log('beforeAll')
});
beforeEach(() => {
console.log('beforeEach')
count = new Counter();
});
afterEach(() => {
console.log('afterEach')
});
afterAll(() => {
console.log('afterAll');
});
// 將 add 類(lèi)型進(jìn)行分組
describe('測(cè)試 add 類(lèi)型用例', () => {
// 在 describe 方法中,鉤子函數(shù)會(huì)按照層級(jí)嵌套進(jìn)行執(zhí)行,先執(zhí)行外部,再執(zhí)行內(nèi)部,不同的 describe 互不干擾
beforeEach(() => {
console.log('beforeEach add');
});
test('測(cè)試加法', () => {
console.log('add')
count.add();
expect(count.number).toBe(1);
})
})
// 將 minus 類(lèi)型進(jìn)行分組
describe('測(cè)試 minus 類(lèi)型用例', () => {
test('測(cè)試減法', () => {
console.log('minus')
count.minus();
expect(count.number).toBe(-1);
})
})
})
加上 describe 方法的執(zhí)行效果如下圖:
Mock
在日常開(kāi)發(fā)中,當(dāng)前端開(kāi)發(fā)差不多后,后端接口可能還沒(méi)有提供,這個(gè)時(shí)候我們就要用 Mock 數(shù)據(jù)。而 Jest 也有 Mock 方法,用于模擬一些 JavaScript 的函數(shù)等。
我們先來(lái)一個(gè)比較簡(jiǎn)單的 mock.fn
// mock.js
export const runFn = (fn) => {
fn(123);
}
// mock.test.js
test('測(cè)試 runFn', () => {
// 通過(guò) jest 的 fn 方法創(chuàng)建一個(gè)模擬函數(shù),如果不傳參數(shù)會(huì)默認(rèn)生成一個(gè)函數(shù)
// 1. 通過(guò) func.mock 獲取想要的值
// 2. 可以自定義返回值
// 3. 改變內(nèi)部函數(shù)的實(shí)現(xiàn),模擬接口請(qǐng)求,不請(qǐng)求代碼中的接口
const func = jest.fn( () => 456 );
// 還可以使用 mockReturnValueOnce 方法進(jìn)行控制輸出,兩種方法都使用時(shí)會(huì)覆蓋 fn 方法中的返回值,支持鏈?zhǔn)秸{(diào)用
// 將 Once 去掉與 fn 方法一樣,多次會(huì)返回相同的值
func.mockReturnValueOnce('zoo')
// 返回 this 方法 mockReturnThis
func.mockReturnThis();
// 還可以使用 mockImplementation 方法書(shū)寫(xiě)函數(shù)內(nèi)部,可以在函數(shù)內(nèi)部寫(xiě)邏輯,與 jest.fn 方法的參數(shù)一樣,還可以填加 Once
func.mockImplementation(() => {
return '123';
})
// 執(zhí)行被測(cè)函數(shù)
runFn(func);
runFn(func);
// console.log(func.mock)
// 因?yàn)楸徽{(diào)用了兩次,所以長(zhǎng)度都是 2
// {
// calls: [ [123], [123] ], // 每次的調(diào)用情況,傳遞的參數(shù)是什么
// instances: [ undefined, undefined ], // 每次調(diào)用的 this 指向,被調(diào)用了幾次
// invocationCallOrder: [ 1, 2 ], // 執(zhí)行順序,可能會(huì)傳入同一個(gè)或多個(gè)方法中,需要記錄一下順序
// results: [ // mock 函數(shù)每次執(zhí)行后的返回值
// { type: 'return', value: 456 },
// { type: 'return', value: 456 }
// ]
// }
// 通過(guò) toBeCalled 判斷函數(shù)是否被調(diào)用
expect(func).toBeCalled();
// 判斷當(dāng)前函數(shù)調(diào)用了幾次 被調(diào)用了兩次
expect(func.mock.calls.length).toBe(2);
// 判斷參數(shù)是什么
expect(func.mock.calls[0]).toEqual([123]);
// 判斷每次調(diào)用的時(shí)候參數(shù)是什么
expect(func).toBeCalledWith(123);
// 判斷返回值
expect(func.mock.results[0].value).toBe('zoo');
})
Mock 高階用法
如果需要通過(guò)修改請(qǐng)求的方式進(jìn)行測(cè)試,而不使用測(cè)試框架,我們可能需要修改請(qǐng)求的代碼邏輯。但是,Jest 提供了一種高級(jí)的 Mock 方法。我們只需在項(xiàng)目根目錄下創(chuàng)建一個(gè)名為 __mocks__ 的文件夾,然后在其中自定義文件內(nèi)容并導(dǎo)出,就可以使用自己定義的 Mock 函數(shù)而不必修改請(qǐng)求代碼邏輯。
書(shū)寫(xiě)測(cè)試用例文件,引入 __mocks__ 文件夾中的函數(shù)
// mocker.test.js
// 使用 mock 方法引用 __mocks__ 下創(chuàng)建的 mock.js
jest.mock("./mock");
// 執(zhí)行完上面的方法,會(huì)直接尋找 __mocks__ 下的getData,而不是正常的請(qǐng)求文件
// 由于 mock 中沒(méi)有 getCode 方法,最好只 mock 異步函數(shù),同步函數(shù)直接測(cè)試即可
// 可以不必須創(chuàng)建 __mocks__ 文件夾
import {
getData,
} from "./mock";
// 需要使用下面的 requireActual 方法來(lái)引用非 mock 文件夾下的 getCode
const { getData } = jest.requireActual("./mock");
// 高階mock
// 此處直接使用 __mocks__ 目錄下的 mock 文件中的函數(shù)
test("測(cè)試 getData", () => {
return getData().then((data) => {
expect(eval(data)).toEqual("123");
});
});
Mock-timers
在特定的業(yè)務(wù)中,需要使用到定時(shí)器,測(cè)試的時(shí)候也是需要修改代碼來(lái)測(cè)試不同時(shí)間,最主要的一點(diǎn)是,我們需要等時(shí)間才能看到我們的執(zhí)行結(jié)果,Jest 也有關(guān)于定時(shí)器的 Mock 函數(shù)。
// mock.js
export const timer = (fn) => {
setTimeout(() => {
fn();
setTimeout(() => {
fn();
}, 3000)
}, 3000)
}
// mock-timers.test.js
import { timer } from './mock';
// 使用 useFakeTimers 方法告知 Jest 在下面的測(cè)試用例,如果用到了定時(shí)器異步函數(shù)的時(shí)候,都是用假的 timers 進(jìn)行模擬
jest.useFakeTimers();
test('測(cè)試 timer', () => {
const fn = jest.fn();
timer(fn);
// 使用 runAllTimers 方法,讓定時(shí)器立即執(zhí)行,和 useFakeTimers 配合使用
jest.runAllTimers();
// 如果代碼中有多個(gè)定時(shí)器嵌套,只想測(cè)試最外層的定時(shí)器,則需要使用 runOnlyPendingTimers 方法
// 這個(gè)方法會(huì)只執(zhí)行當(dāng)前在隊(duì)列中的函數(shù),可以多次調(diào)用
jest.runOnlyPendingTimers();
jest.runOnlyPendingTimers();
// advanceTimersByTime 方法,可以快進(jìn)時(shí)間
// 因?yàn)?timer 中,三秒后只執(zhí)行了第一層,如果是六秒,則會(huì)執(zhí)行兩次 fn
jest.advanceTimersByTime(3000);
})
snapshot 快照
到這里我們已經(jīng)可以測(cè)試一些代碼了,但是我們要如何捕捉執(zhí)行結(jié)果和當(dāng)前做對(duì)比呢?這時(shí)候就要使用快照功能了。
// snapshot.js
export const config1 = () => {
return {
method: 'GET',
url: '/api',
time: new Date()
}
}
export const config2 = () => {
return {
method: 'GET',
url: '/api',
time: new Date().getTime()
}
}
// snapshot.test.js
import { config1, config2 } from "./snapshot";
test('測(cè)試 config1 返回值', () => {
// 但如果每次函數(shù)修改的時(shí)候,當(dāng)前測(cè)試用例也要不斷地修改
// expect(config()).toEqual({
// method: 'GET',
// url: '/api'
// });
// 需要使用快照匹配 toMatchSnapshot 方法
// 此方法會(huì)生成一個(gè) __snapshots__ 目錄,下面的文件中,第一次執(zhí)行中 config 生成的結(jié)果會(huì)存到快照文件中
// 快照會(huì)根據(jù) test 方法中的描述生成一個(gè)映射關(guān)系
// 修改后的 config 的執(zhí)行結(jié)果與快照中的結(jié)果不同時(shí)會(huì)報(bào)錯(cuò),需要更新快照
// 如果 config 中有的值是每次運(yùn)行都會(huì)變化的,那么每次快照都不會(huì)與當(dāng)前執(zhí)行相同,除非執(zhí)行后再更新快照
// 需要將在 toMatchSnapshot 方法中傳遞一個(gè)參數(shù),設(shè)置一下 time 為任意格式的 Date 類(lèi)型
expect(config1()).toMatchSnapshot({
time: expect.any(Date)
});
})
test('測(cè)試 config2 返回值', () => {
expect(config2()).toMatchSnapshot({
time: expect.any(Number)
});
})
行內(nèi)快照生成
// snapshot.test.js
// 需要安裝 prettier
test("測(cè)試 config2 返回值", () => {
// toMatchInlineSnapshot 方法,將執(zhí)行快照放到行內(nèi)中,會(huì)放到 toMatchInlineSnapshot 方法中
expect(config2()).toMatchInlineSnapshot(
{
time: expect.any(Number)
},
`
Object {
"method": "GET",
"time": Any<Number>,
"url": "/api",
}
`
);
});
對(duì) dom 節(jié)點(diǎn)測(cè)試
Jest 內(nèi)部自己模擬了一套 jsDom ,可以在 node 的環(huán)境下執(zhí)行需要瀏覽器環(huán)境 dom 的測(cè)試用例。
// dom.js
import $ from 'jquery';
const addDiv = () => {
// jQuery
$('body').append('<div/>');
}
export default addDiv;
// dom.test.js
import addDiv from './dom';
import $ from 'jquery';
test('測(cè)試 addDiv', () => {
addDiv();
addDiv();
console.log($('body').find('div').length);
// 測(cè)試dom
expect($('body').find('div').length).toBe(2);
expect(document.getElementsByTagName('div').length).toBe(2);
})
VSCode 插件
Jest Snippets 用于快速生成 Jest 代碼塊的工具。
Jest 能夠檢測(cè)當(dāng)前文件夾中的測(cè)試用例并自動(dòng)運(yùn)行測(cè)試,還支持可視化操作,更新、執(zhí)行以及單個(gè)執(zhí)行等功能,非常方便!
常用配置解讀
module.exports = {
// 檢測(cè)從哪個(gè)目錄開(kāi)始,rootDir 代表根目錄
roots: ["<rootDir>/src"],
// 代碼測(cè)試覆蓋率通過(guò)分析那些文件生成的,!代表不要分析
collectCoverageFrom: [
// src 下所有 js jsx ts tsx 后綴的文件
"src/**/*.{js,jsx,ts,tsx}",
// src 下所有 .d.ts 后綴的文件
"!src/**/*.d.ts"
],
// 運(yùn)行測(cè)試之前,我們額外需要準(zhǔn)備什么
setupFiles: ["react-app-polyfill/jsdom"],
// 當(dāng)測(cè)試環(huán)境建立好后,需要做其他事情時(shí)可以引入對(duì)應(yīng)的文件
setupFilesAfterEnv: ["<rootDir>/src/setupTests.js"],
// 哪些文件會(huì)被認(rèn)為測(cè)試文件
testMatch: [
// src 下的所有 __tests__ 文件夾中的所有的 js jsx ts tsx 后綴的文件都會(huì)被認(rèn)為是測(cè)試文件
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
// scr 下的所有一 .test/spec.js/jsx/ts/tsx 后綴的文件都會(huì)被認(rèn)為是測(cè)試文件
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}",
],
// 測(cè)試運(yùn)行的環(huán)境,會(huì)模擬 dom
testEnvironment: "jsdom",
// 測(cè)試文件中引用一下后綴結(jié)尾的文件會(huì)使用對(duì)應(yīng)的處理方式
transform: {
// 如果引用的是 js jsx mjs cjs ts tsx 后綴的文件會(huì)使用 /config/jest/babelTransform.js 文件進(jìn)行處理
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/config/jest/babelTransform.js",
// 如果引用的是 css 后綴的文件,會(huì)使用 /config/jest/cssTransform.js 文件處理
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
// 不是以 js jsx mjs cjs ts tsx css json 這些為后綴的文件會(huì)使用 /config/jest/fileTransform.js 文件進(jìn)行處理
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)":
"<rootDir>/config/jest/fileTransform.js",
},
// 忽略 transform 配置轉(zhuǎn)化的文件
transformIgnorePatterns: [
// node_modules 目錄下的 js jsx mjs cjs ts tsx 后綴的文件都不需要轉(zhuǎn)化
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
// .module.css/sass/scss 后綴的文件都不需要轉(zhuǎn)化
"^.+\\.module\\.(css|sass|scss)$",
],
// 自動(dòng)化測(cè)試時(shí),應(yīng)用的模塊應(yīng)該從哪里尋找,默認(rèn)是在 node_modules
modulePaths: [],
// 模塊名字使用哪種工具進(jìn)行映射
moduleNameMapper: {
// 針對(duì)于 native 移動(dòng)端
// "^react-native$": "react-native-web",
// 將 .module.css/sass/scss 模塊使用 identity-obj-proxy 工具進(jìn)行轉(zhuǎn)化
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
},
// 引入模塊時(shí),進(jìn)行自動(dòng)查找模塊類(lèi)型,逐個(gè)匹配
moduleFileExtensions: [
"web.js",
"js",
"web.ts",
"ts",
"web.tsx",
"tsx",
"json",
"web.jsx",
"jsx",
"node",
],
// 監(jiān)聽(tīng)插件
watchPlugins: [
"jest-watch-typeahead/filename",
"jest-watch-typeahead/testname",
],
// 重置 mock
resetMocks: true,
};
小結(jié)
在實(shí)際業(yè)務(wù)應(yīng)用中,我們建議對(duì)可復(fù)用的組件、工具函數(shù)、工具類(lèi)等一些無(wú)副作用,可預(yù)知結(jié)果的代碼來(lái)進(jìn)行單元測(cè)試。在前期開(kāi)發(fā)過(guò)程中的投入會(huì)大于沒(méi)有單元測(cè)試的投入,因?yàn)橐獙?xiě)一些測(cè)試用例,還要執(zhí)行測(cè)試用例,優(yōu)化代碼等。但是在長(zhǎng)久迭代中,這種方法會(huì)比沒(méi)有進(jìn)行單元測(cè)試的模塊更加穩(wěn)定。
代碼地址
- 前置 demo :https://github.com/Jadony/Jest-demo
- Jest 簡(jiǎn)單配置:https://github.com/Jadony/jest-config
- Jest 匹配器:https://github.com/Jadony/jest-matchers
- 異步代碼測(cè)試:https://github.com/Jadony/jest-async
- Jest 鉤子函數(shù):https://github.com/Jadony/jest-hook
- Jest 的 mock 函數(shù):https://github.com/Jadony/jest-mock
- Jest 的快照:https://github.com/Jadony/jest-snapshot
- Jest 對(duì) Dom 節(jié)點(diǎn)的測(cè)試:https://github.com/Jadony/jest-dom
參考文檔
- 《前端要學(xué)的測(cè)試課 從Jest入門(mén)到TDD/BDD雙實(shí)戰(zhàn)》(https://coding.imooc.com/class/chapter/372.html#Anchor)