Jest 前端測試框架

  • 目前在專案中希望能夠配合 CI/CD,以進行自動化測試。除了對後端進行自動化測試外,也希望能針對前端進行測試。

  • 因此,我寫了一個使用 Jest 框架來建立前端測試環境的專案。未來各專案可以 clone 這份專案,並與 CI/CD 配合進行前端測試的開發。

  • 在這個專案中,順手記錄了一些 Jest 框架應用,以及前端測試可能會用到的方法。

─ 如何安裝 Jest

  • 系統環境中需要先安裝 Node.js 和 npm。

  • 終端機在你的專案路徑下輸入初始化 npm 的指令。
    • npm init -y

  • 安裝 Jest
    • npm install –save-dev jest

  • 使用 npm 輸入安裝 Jest 的指令之後,除了在 node_module 中加入 jest 模組之外,也會在 package.json 文件中加入一些基本的設定。 安裝 Jest

─ 專案結構

以下是 Jest 專案常見的結構。

  • test_project/
    • src/
      • components/
        • Component1.js
        • Component2.js
      • utils/
        • util1.js
        • util2.js
    • tests/
      • unit/
        • component1.test.js
        • component2.test.js
      • integration/
        • integration_test1.test.js
        • integration_test2.test.js
    • coverage/
      • lcov-report/
        • index.html
    • node_modules/
    • package.json
    • setupTests.js

  • src/:這裡存放需要被測試的原始碼文件。
  • tests/:這裡存放針對 src/ 內程式碼撰寫的測試案例文件。
  • coverage/:這裡存放測試後生成的程式碼覆蓋率報告文件。
  • package.json:這個檔案包含了與測試相關的配置設定。

那 setupTests.js 則是你在配置的時候,如果有定義 setupFiles,那你就可以在這支檔案裡面做一些測試前的準備工作。
專案結構

─ 測試相關配置

─── package.json

  • Jest 的 package.json 可以設定測試的相關配置,這些配置通常包括測試的入口文件、測試覆蓋率報告、以及其他一些自定義配置。
{
  "name": "your-project-name",
  "version": "1.0.0",
  "description": "Description of your project",
  "scripts": {
  "test": "jest"
  },
  // 專案運行時所需的環境依賴
  "devDependencies": {
    "jest": "^29.7.0",
    "jsdom": "^24.0.0",
    "jest-mock-axios": "^4.7.3",
    "jest-environment-jsdom": "^29.7.0"
  },
  // 專案運行時所需的外部依賴
  "dependencies": {
    "jquery": "^3.7.1"
  },
  // 定義 Jest 測試 option
  "jest": {
    "rootDir": "../",
    "roots": [
      "<rootDir>/test_project"
    ],
    // 指定在執行測試之前要執行的腳本文件
    "setupFiles": [
      "<rootDir>/test_project/setupTests.js"
    ],
    // 指定 Jest 測試的環境
    "testEnvironment": "jsdom",
    // 指定 Jest 測試文件的匹配模式
    "testMatch": [
      "**/__tests__/**/*.js", 
      "**/?(*.)+(spec|test).js"
    ],
    // 定義覆蓋率的閾值
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    },
    // 指定哪些文件需要收集覆蓋率訊息
    "collectCoverageFrom": [
      "src/**/*.js"
    ],
    // 指定生成覆蓋率報告的格式
    "coverageReporters": [
      "json",
      "lcov",
      "text",
      "clover"
    ],
    // 指定覆蓋率報告的輸出目錄
    "coverageDirectory": "coverage",
  }
}
  • devDependencies : 在這個節點中,列出了專案運行時所需的環境依賴。
    • 這邊可以使用 npm install –save-dev jest 或 yarn add –dev jest 來安裝 Jest。

  • dependencies : 在這個節點中,列出了專案運行時所需的外部依賴。
    • 這邊可以使用 npm install 來安裝第三方 module。

  • jest : 在這個節點中,定義了 Jest 測試的配置選項:
    • setupFiles: 指定在執行測試之前要執行的腳本文件。
      • 可以在執行測試之前,預先執行一些全域設置、初始化或配置測試環境等。
        全域設置
    • testEnvironment: 指定 Jest 測試的環境。
    • testMatch: 指定 Jest 測試文件的匹配模式。
    • coverageThreshold: 定義代碼覆蓋率的閾值。
    • collectCoverageFrom: 指定哪些文件需要收集代碼覆蓋率訊息。
    • coverageReporters: 指定生成覆蓋率報告的格式。
    • coverageDirectory: 指定覆蓋率報告的輸出目錄。

  • 上述是 package.json 配置中的一些常見選項,根據具體需求,可以在加入其他配置。

─── jest.config.js

  • Jest 運行測試時,會從專案目錄開始尋找 jest.config.js ,如果沒有找到,就會查看上面說到的 package.json 其中是否包含 jest 配置區塊。

  • 而使用 JavaScript 來定義配置,可以使用一些變數、條件邏輯、或是可以直接使用 require 載入其他模組當作設置等等,比起簡單的 JSON 格式可以更靈活的配置你的測試環境,你可以根據不同的專案需求或結構,來選擇適合的配置方式。

// jest.config.js
module.exports = {
    rootDir: "../",
    roots: ["<rootDir>/test_project"],
    setupFiles: ["<rootDir>/test_project/setupTests.js"],
    testEnvironment: "jsdom",
    testMatch: [
        "**/__tests__/**/*.js", 
        "**/?(*.)+(spec|test).js"
    ],
    coverageDirectory: "<rootDir>/test_project/Coverage/",
    reporters: [
        "default",
        [require.resolve('jest-junit'), {
            "outputDirectory": "<rootDir>/test_project/Coverage/",
            "outputName": "jest-junit.xml"
        }]
    ]
};

─ 新增測試檔案

  1. 假設在 src/ 路徑下,有一個 calculator.js 需要被測試的檔案。

    • test_project/
      • src/
        • calculator.js
      • tests/

  2. 需要將 calculator.js 內部方法使用 module.exports 導出。
     // calculator.js
     const calculator = {
         function() {
             //...
         }
     };
    
     module.exports = calculator;
    
  3. 再 tests/ 路徑下,新增名為 calculator.test.js 的測試檔案。
    • test_project/
      • src/
        • calculator.js
      • tests/
        • calculator.test.js

  4. 在 calculator.test.js 測試檔案檔案內,引入被測試檔案。
     // calculator.test.js
     const Calculator = require('calculator');
    
  5. 針對被測試檔案內部方法撰寫測試。
     // calculator.test.js
     const Calculator = require('calculator');
    
     describe('testModule', () => {
       it('TestFunction', () => {
         //...
         });
    
       it('innerFunction', () => {
         //...
       });
     });
    

─ 如何運行測試

  • 在 Command-line (CMD) 運行測試

    • 打開(CMD)使用 cd 命令進入你的專案目錄。

    • 如果在 package.json 中設置了測試腳本,一旦進入了專案目錄,可以直接運行測試。

        cd path/to/your/project
      
    • 或是輸入指令進行測試,這將會運行所有的測試並輸出結果到終端。
        npm test
      
        npm test -- --coverage        # --coverage 會顯示覆蓋率
      
    • 預設情況下,Jest 會找:tests 資料夾內的 .js, .jsx, .ts, .tsx 以 .test 或 .spec 結尾的檔案。
      • 例如 component1.test.js

測試方法

─ 測試案例

  • describe
    • 組織測試案例:
      • 使用 describe 函數來將測試案例分組,使其更有組織性和易讀性。

    • 提供描述標題:
      • 每個 describe 區塊有一個描述性的標題,可以清楚地說明這組測試案例的目的或主題。

    • 隔離測試案例:
      • 使用 describe 函數將相關的測試案例分組,這樣可以更好地隔離測試案例之間的影響。

  • it / test
    • 提供測試案例的描述:
      • 通過標題清楚地描述測試的目的或要驗證的功能。

    • 提供可讀性和可維護性:
      • 通過清晰的描述和組織良好的測試案例,提高測試的可讀性和可維護性。

describe('testModule', () => {
    it('TestFunction', () => {
        //...
    });
    
    it('innerFunction', () => {
        //...
    });
});

─ HOOK

  • 在 Jest 測試框架中有提供鉤子函數,beforeEach & afterEach。
    • beforeEach & afterEach 的觸發會限制在 describe 群組內

  • beforeEach
    • 函數會在每個測試運行之前運行一次。
    • 通常用於設置測試的前置條件。
    • 例如:初始化測試數據、創建模擬對象等
        describe('testModule', () => {
            beforeEach(() => {
                // 在每個測試運行之前執行的操作
                // 初始化測試數據、創建模擬對象等
            });
                  
            it('TestFunction', () => {
                //...
            });
        });
      


  • afterEach
    • 函數會在每個測試運行之後運行一次。
    • 通常用於清理測試過程中可能產生的副作用。
    • 例如:重置測試數據、銷毀模擬對象等。
        describe('testModule', () => {
            afterEach(() => {
                // 在每個測試運行之後執行的操作
                // 清理測試過程中可能產生的副作用、重置測試數據、銷毀模擬對象等
            });
              
            it('TestFunction', () => {
                //...
            });
        });
      

─ 監視 & 斷言

  • 而該怎麼驗證測試是否正確,Jest 提供 jest.spyOn() 函式。

    • jest.spyOn() 用於創建一個對特定物件的方法進行模擬的 Spy(間諜)。

    • jest.spyOn() 也可以監視一個物件的特定方法,記錄它的呼叫情況以及接收到的參數。

        // Module.test.js
        // 引入被測試的模組
        const Module = require('./Module');
      
        // 使用 jest.spyOn 監視 TestFunction 方法
        const spy = jest.spyOn(Module, 'TestFunction');
      
        // 呼叫 TestFunction 方法
        Module.TestFunction();
      
        // 斷言 TestFunction 方法被呼叫過
        expect(spy).toHaveBeenCalled();
      
        // 清除監視,恢復原始方法
        spy.mockRestore();
      


  • 在上述 jest.spyOn 監視物件方法後,可以使用 Jest 提供的 expect() 斷言函式來進行測試

    • 返回值的斷言方法
      1. expect().toBe(value): 斷言實際值是否與預期值完全相等。
      2. expect().toEqual(value): 斷言兩個對象是否在值上相等(所有屬性和屬性值相等)。
      3. expect().toMatch(pattern): 斷言字符串是否與正則表達式匹配。
      4. expect().toContain(item): 斷言某個集合(數組、Set)是否包含指定的元素。
      5. more

    • 監視的斷言方法
      1. toHaveBeenCalled(): 斷言被監視的方法是否被呼叫過。
      2. toHaveBeenCalledWith(arg1, arg2, …): 斷言被監視的方法是否被指定的參數呼叫過。
      3. toHaveReturned(): 斷言被監視的方法是否有返回值。
      4. toHaveReturnedWith(value): 斷言被監視的方法是否有指定的返回值。
      5. toHaveBeenCalledTimes(number): 斷言監視方法被呼叫的次數是否符合指定的數量。
      6. more

─ 模擬方法行為

  • mockReturnValue
    • mockReturnValue 用於設置 mock 函數的返回值。它將覆蓋原始函數的返回值,無論函數的實際執行結果是什麼,都將返回被設置的值。
    • 通常與 jest.fn() 一起使用,用於創建一個新的 mock 函數並設置其返回值。
    • 當你想要模擬函數的返回值,但不關心函數的實際實現時,可以使用 mockReturnValue。

  • mockImplementation
    • mockImplementation 用於設置 mock 函數的實現。它將覆蓋原始函數的實現,無論原始函數的實際代碼是什麼,都將執行被設置的函數實現。
    • 通常與 jest.fn() 一起使用,用於創建一個新的 mock 函數並設置其實現。
    • 當你想要模擬函數的實現,並指定特定的行為或邏輯時,可以使用 mockImplementation。

  • 當測試某個方法時,該方法內部調用了其他方法並依賴其回傳值時,使用模擬回傳值的方法來測試。
    • 隔離被測試方法的行為,專注於測試它的功能,而不需要依賴其他方法的實際實現。
    • 可以使測試更加獨立和可靠,並且在測試失敗時更容易定位問題。

      // Module.js
      function TestFunction() {
        // 假設 TestFunction 內部調用了 innerFunction 方法
        const value = innerFunction();
        return value;
      }
    
      function innerFunction() {
        //...內部處理邏輯
      }
    
      module.exports = {
        TestFunction: TestFunction,
        innerFunction: innerFunction
      };
    
      // Module.test.js
      const Module = require('./Module');
    
      // 模擬 innerFunction 的返回值
      const innerSpy = jest.spyOn(Module, 'innerFunction').mockReturnValue('jensen');
    
      // 使用 jest.spyOn 監視 TestFunction 方法
      const spy = jest.spyOn(Module, 'TestFunction');
    
      // 調用 TestFunction 方法
      const result = Module.TestFunction();
    
      // 斷言方法的返回值是否正確
      expect(result).toBe('jensen');
    
      // 清除監視
      spy.mockRestore();
      innerSpy.mockRestore();
    

─ 清除 & 恢復

  • 在測試結束後要清除監視,恢復原始方法,避免引響其他測試,確保測試獨立性。

    1. jest.clearAllMocks(): 清除所有模擬函數的調用次數、傳入的參數等,但不還原模擬函數的行為。
    2. jest.clearAllTimers(): 清除所有計時器的設置,包括 setTimeout、setInterval 等。
    3. jest.resetAllMocks(): 重置所有模擬函數的狀態,包括還原它們的行為以及清除調用信息。
    4. jest.resetModules(): 重置所有模組的狀態,將它們從快取中卸載並重新加載,使它們回到初始狀態。
    5. jest.restoreAllMocks(): 還原所有使用 jest.spyOn 創建的模擬函數的原始實現。
    6. jest.runOnlyPendingTimers(): 立即執行所有處於待定狀態的計時器,而不需要等待真實的時間。
    7. jest.advanceTimersByTime(ms): 快進指定時間(ms)以觸發計時器。

覆蓋率分析

─ Command

  • 當執行測試之後在在 Terminal 畫面上會顯示目前被測程式,測試到的覆蓋率 %。
    覆蓋率

  • 當中可以看到測試案例是成功或是失敗,最後會統計整體與個別的覆蓋率資訊。
    終端機覆蓋率

─ HTML

  • 執行測試完畢的同時在 Coverage 資料夾下的 lcov-report 裡,也會生成一個 index.html。
    index網頁

  • 裡面提供了更詳細的圖表分析報告。
    總分析報告

  • 可以針對個別檔案點擊,查看詳細的內容。

    • 內容會以紅色標註指出未覆蓋到的 Statements。

    • 而在判斷處會指出其中 ifelse 沒有被測試到。

    • 能夠更精確的撰寫測試案例,以便覆蓋到所有 Statements。

    個別分析報告


Reference