隨著JavaScript在前後端開發中的廣泛應用,測試已成為保證代碼質量的關鍵環節。 為什麼需要單元測試 在我們的開發過程中,經常需要定義一些演算法函數,例如將介面返回的數據轉換成UI組件所需的格式。為了校驗這些演算法函數的健壯性,部分開發同學可能會手動定義幾個輸入樣本進行初步校驗,一旦校驗通過便不再深 ...
隨著JavaScript在前後端開發中的廣泛應用,測試已成為保證代碼質量的關鍵環節。
為什麼需要單元測試
在我們的開發過程中,經常需要定義一些演算法函數,例如將介面返回的數據轉換成UI組件所需的格式。為了校驗這些演算法函數的健壯性,部分開發同學可能會手動定義幾個輸入樣本進行初步校驗,一旦校驗通過便不再深究。
然而,這樣的做法可能會帶來一些潛在的問題。首先,邊界值的情況往往容易被忽視,導致校驗不夠全面,增加了系統出現故障的風險。其次,隨著需求的變化和演進,演算法函數可能需要進行優化和擴展。如果前期的校驗工作不夠徹底,不瞭解現有函數覆蓋的具體場景,就可能導致在後續的修改中引入新的問題。
單元測試可以有效地解決上述問題。在定義演算法函數時,同步創建單元測試文件,並將可能出現的各種場景逐一列舉。如果單元測試未能通過,項目在編譯時會直接報錯,從而能夠及時發現並針對性地解決問題。此外,當後續有新同學加入並需要擴展功能時,他們不僅需要在原有的單元測試基礎上添加新的測試用例,還能確保新功能的正確性,同時保障原有功能的正常運行。
自定義測試邏輯
在開始使用工具來進行單元測試之前,我們可以先自定義一個工具函數供測試使用。
例如,我們有一個 add
函數,期望它能夠正確計算兩個數的和,並驗證其結果是否符合預期。比如,我們希望驗證 2 + 3
的結果是否等於 5
,可以使用 expect(add(2, 3)).toBe(5)
這樣的代碼來實現。為此,我們可以自行定義一個expect
函數,使其具備類似Jest中 expect
函數的功能
function add(a, b) { return a + b; }
function expect(result) {
return {
toBe(value) {
if (result === value) {
console.log("驗證成功");
} else {
throw new Error(`執行錯誤:${result} !== ${value}`);
}
},
};
}
// 調用示例
try {
expect(add(2, 3)).toBe(5); // 輸出:"驗證成功"
expect(add(2, 3)).toBe(6); // 拋出錯誤
} catch (err) {
console.error(err.message); // 輸出:"執行錯誤:5 !== 6"
}
為了使測試更具描述性和可讀性,我們可以進一步增強我們的測試邏輯。例如,我們可以添加一個 test
函數,用於描述測試的目的,併在測試失敗時提供更詳細的錯誤信息。
function test(description, fn) {
try {
fn();
console.log(`測試通過: ${description}`);
} catch (err) {
console.error(`測試失敗: ${description} - ${err.message}`);
}
}
// 調用示例
test("驗證 2 + 3 是否等於 5", () => {
expect(add(2, 3)).toBe(5);
});
test("驗證 2 + 3 是否等於 6", () => {
expect(add(2, 3)).toBe(6);
});
通過這種方式,我們模擬了一個簡單的測試用例,其中 test
和 expect
函數類似於Jest中的功能。然而,我們的自定義版本相對簡陋,缺乏 Jest
提供的豐富功能。
Jest
通過上述示例,我們可以瞭解到編寫測試的基本思路和方法。然而,在實際開發中,我們需要一個功能更加強大、易用性更高的測試工具。Jest
正是這樣一個工具,它不僅提供了豐富的匹配器
(如toBe、toEqual等),還支持非同步測試
、Mock函數
、Snapshot測試
等功能。
引入 Jest
的依賴後,我們可以直接使用其內置的 test
和 expect
函數,從而大大提高測試的效率和準確性。Jest
的強大之處在於它能夠幫助我們全面地覆蓋各種測試場景,並提供詳細的錯誤報告,使我們能夠快速定位和解決問題。
初始化
首先,我們通過 npm install jest -D
安裝 Jest
依賴,然後執行 npx jest --init
。此時,命令行工具會出現一系列互動式問答,詢問你是否要為 Jest 添加名為 test 的腳本指令、是否使用 TypeScript 作為配置文件、測試用例執行環境、是否需要代碼覆蓋率測試報告、生成測試報告的平臺的編譯器以及是否需要在每次測試用例執行前重置 Mock 函數狀態。
完成所有問答後,Jest 會修改 package.json
文件,並生成jest.config.js
配置文件。在執行測試用例時,將依據這些配置項進行。
我們創建一個 math.test.js
文件,並將之前的測試代碼放入其中
function add(a, b) {
return a + b;
}
test("測試 add 函數", () => {
expect(add(2, 3)).toBe(5);
});
通過 npm run test
執行 Jest 運行指令,可以在命令行工具查看詳細的測試信息,包括哪個文件的哪條測試用例的狀態,以及簡易的測試覆蓋率報告。
在實際使用場景中,add
函數通常定義在項目文件中,並通過 ES 模塊化
(export 和 import) 方式導出和導入。預設情況下,Jest 並不支持 ES 模塊化語法,因此我們需要通過 Babel 進行配置。
首先,執行以下命令安裝 Babel 及其核心庫和預設
npm install @babel/core @babel/preset-env --save-dev
然後,創建babel.config.js
文件並定義配置
module.exports = {
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current",
},
},
],
],
};
接著,將 add
函數移到 math.js
文件中,並使用 export
導出
// math.js
export function add(a, b) {
return a + b;
}
最後,在 math.test.js 文件中使用 import 導入
// math.test.js
import { add } from './math';
test("測試 add 函數", () => {
expect(add(2, 3)).toBe(5);
});
通過以上步驟,你就完成了使用 Jest 執行 ES 模塊化代碼的環境初始化。
匹配器
Jest 中最常用的功能之一就是匹配器。在前面進行測試時,我們就接觸過 toBe
這一匹配器,它用於判斷值是否相等。除此之外,還有許多其他類型的匹配器。
值相等
判斷值相等有兩種匹配器:toBe
和 toEqual
。對於基本數據類型(如字元串、數字、布爾值),兩者的使用效果相同。但對於引用類型(如對象和數組),toBe
只有在兩個引用指向同一個記憶體地址時才會返回 true
。
const user = { name: "alice" };
const info = { name: "alice" };
test("toEqual", () => {
expect(info).toEqual(user); // 通過,兩者結構相同
});
test("toBe", () => {
expect(info).toBe(user); // 不通過,兩者的引用地址不同
});
是否有值
存在 toBeNull
、toBeUndefined
和 toBeDefined
匹配器來分別判斷值是否為 null、未定義或已定義。
test("toBeNull", () => {
expect(null).toBeNull();
expect(0).toBeNull(); // 不通過
expect("hello").toBeNull(); // 不通過
expect(undefined).toBeBull(); // 不通過
});
test("toBeUnDefined", () => {
expect(null).toBeUndefined(); // 不通過
expect(0).toBeUndefined(); // 不通過
expect("hello").toBeUndefined(); // 不通過
expect(undefined).toBeUndefined();
});
test("toBeDefined", () => {
expect(null).toBeDefined();
expect(0).toBeDefined();
expect("hello").toBeDefined();
expect(undefined).toBeDefined(); // 不通過
});
是否為真
toBeTruthy
用於判斷值是否為真,toBeFalsy
用於判斷值是否為假,not
用於取反。
test("toBeTruthy", () => {
expect(null).toBeTruthy(); // 不通過
expect(0).toBeTruthy(); // 不通過
expect(1).toBeTruthy();
expect("").toBeTruthy(); // 不通過
expect("hello").toBeTruthy();
expect(undefined).toBeTruthy(); // 不通過
});
test("toBeFalsy", () => {
expect(null).toBeFalsy();
expect(0).toBeFalsy();
expect(1).toBeFalsy(); // 不通過
expect("").toBeFalsy();
expect("hello").toBeFalsy(); // 不通過
expect(undefined).toBeFalsy();
});
test("not", () => {
expect(null).not.toBeTruthy();
expect("hello").not.toBeTruthy(); // 不通過
});
數字比較
toBeGreaterThan
用於判斷是否大於某個數值,toBeLessThan
用於判斷是否小於某個數值,toBeGreaterThanOrEqual
用於判斷是否大於或等於某個數值,toBeCloseTo
用於判斷是否接近某個數值(差值 < 0.005)。
test("toBeGreaterThan", () => {
expect(9).toBeGreaterThan(5);
expect(5).toBeGreaterThan(5); // 不通過
expect(1).toBeGreaterThan(5); // 不通過
});
test("toBeLessThan", () => {
expect(9).toBeLessThan(5); // 不通過
expect(5).toBeLessThan(5); // 不通過
expect(1).toBeLessThan(5);
});
test("toBeGreaterThanOrEqual", () => {
expect(9).toBeGreaterThanOrEqual(5);
expect(5).toBeGreaterThanOrEqual(5);
expect(1).toBeGreaterThanOrEqual(5); // 不通過
});
test("toBeCloseTo", () => {
expect(0.1 + 0.2).toBeCloseTo(0.3);
expect(1 + 2).toBeCloseTo(3);
expect(0.1 + 0.2).toBeCloseTo(0.4); // 不通過
});
字元串相關
toMatch
用於判斷字元串是否包含指定子字元串,部分包含即可。
test("toMatch", () => {
expect("alice").toMatch("alice"); // 通過
expect("alice").toMatch("lice"); // 通過
expect("alice").toMatch("al"); // 通過
});
數組相關
toContain
用於判斷數組是否包含指定元素,類似於 JavaScript 中的 includes
方法。
test("toContain", () => {
expect(['banana', 'apple', 'orange']).toContain("apple");
expect(['banana', 'apple', 'orange']).toContain("app"); // 不通過
});
error相關
toThrow
用於判斷函數是否拋出異常,並可以指定拋出異常的具體內容。
test("toThrow", () => {
const throwNewErrorFunc = () => {
throw new TypeError("this is a new error");
};
expect(throwNewErrorFunc).toThrow();
expect(throwNewErrorFunc).toThrow("new error");
expect(throwNewErrorFunc).toThrow("TypeError"); // 不通過
});
以上就是各類型常用的匹配器。
命令行工具
在 package.json
中配置 script
指令,可以使 .test.js
文件在修改時實時自動執行測試用例。
"scripts": {
"jest": "jest --watchAll"
},
在命令行中,你會實時看到當前測試用例的執行結果。同時,Jest 還提供了一些快捷配置,按下 w
鍵即可查看具體有哪些指令。
主要有以下幾種類型:
f 模式
在所有測試用例中,只執行上一次失敗的測試用例。即使其他測試用例的內容有修改,也不會被執行。
o 模式
只執行修改過的測試用例。這個功能需要配合 Git
來實現,根據本次相對於上次 Git 倉庫的更改。這種模式還可以通過配置 script 指令來實現,即:
"script": {
"test": "jest --watch"
}
p模式
當使用 --watchAll
時,修改一個文件的代碼後,所有的測試用例都會執行。進入 p
模式後,可以輸入文件名 matchersFile
,此時修改任何文件只會去查找包含 matchersFile
的文件並執行。
t模式
輸入測試用例名稱,匹配 test 函數的第一個參數。匹配成功後即執行該測試用例。
q模式
退出實時代碼檢測。
通過不同的指令,你可以更有針對性地檢測測試用例。
鉤子函數
在 Jest 中,describe 函數用於將一系列相關的測試用例(tests)組合在一起,形成一個描述性的測試塊。它接受兩個參數:第一個參數是一個字元串,用於描述測試塊的主題;第二個參數是一個函數,包含一組測試用例。
即使沒有顯式定義 describe 函數,每個測試文件也會在最外層預設加上一層 describe 包裹。
在 describe 組成的每個塊中,存在一些鉤子函數,貫穿測試用例的整個過程。這些鉤子函數主要用於測試用例執行之前的準備工作或之後的清理工作。
常用的鉤子函數
- beforeAll 函數在一個 describe 塊開始之前執行一次
- afterAll 函數在一個 describe 塊結束之後執行一次
- beforeEach 函數在每個測試用例之前執行
- afterEach 在每個測試用例之後執行
示例代碼
下麵的示例代碼展示瞭如何使用這些鉤子函數:
describe("測試是否有值", () => {
beforeAll(() => {
console.log("beforeAll");
});
afterAll(() => {
console.log("afterAll");
});
beforeEach(() => {
console.log("beforeEach");
});
describe("toBeNull", () => {
beforeAll(() => {
console.log("toBeNull beforeAll");
});
afterAll(() => {
console.log("toBeNull afterAll");
});
beforeEach(() => {
console.log("toBeNull beforeEach");
});
test("toBeNull", () => {
expect(null).toBeNull();
});
});
});
輸出順序
當運行上述測試用例時,輸出的順序如下:
beforeAll
toBeNull beforeAll
beforeEach
toBeNull beforeEach
toBeNull afterAll
afterAll
通過使用這些鉤子函數,你可以更好地管理測試用例的生命周期,確保每次測試都從一個乾凈的狀態開始,併在測試結束後清理掉產生的副作用。
在這一篇測試指南中,我們介紹了Jest 的背景、如何初始化項目、常用的匹配器語法、鉤子函數。下一篇篇將繼續深入探討 Jest 的高級特性,包括 Mock 函數、非同步請求的處理、Mock 請求的模擬、類的模擬以及定時器的模擬、snapshot 的使用。通過這些技術,我們將能夠更高效地編寫和維護測試用例,尤其是在處理複雜非同步邏輯和外部依賴時。