全面掌握 Jest:從零開始的測試指南(上篇)

一颗冰淇淋發表於2024-09-17

隨著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);
});

透過這種方式,我們模擬了一個簡單的測試用例,其中 testexpect 函式類似於Jest中的功能。然而,我們的自定義版本相對簡陋,缺乏 Jest 提供的豐富功能。

Jest

透過上述示例,我們可以瞭解到編寫測試的基本思路和方法。然而,在實際開發中,我們需要一個功能更加強大、易用性更高的測試工具。Jest 正是這樣一個工具,它不僅提供了豐富的匹配器(如toBe、toEqual等),還支援非同步測試Mock函式Snapshot測試 等功能。

引入 Jest 的依賴後,我們可以直接使用其內建的 testexpect 函式,從而大大提高測試的效率和準確性。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 這一匹配器,它用於判斷值是否相等。除此之外,還有許多其他型別的匹配器。

值相等

判斷值相等有兩種匹配器:toBetoEqual。對於基本資料型別(如字串、數字、布林值),兩者的使用效果相同。但對於引用型別(如物件和陣列),toBe 只有在兩個引用指向同一個記憶體地址時才會返回 true

const user = { name: "alice" };
const info = { name: "alice" };

test("toEqual", () => {
  expect(info).toEqual(user); // 透過,兩者結構相同
});
test("toBe", () => {
  expect(info).toBe(user); // 不透過,兩者的引用地址不同
});
是否有值

存在 toBeNulltoBeUndefinedtoBeDefined 匹配器來分別判斷值是否為 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 的使用。透過這些技術,我們將能夠更高效地編寫和維護測試用例,尤其是在處理複雜非同步邏輯和外部依賴時。

相關文章