動態

詳情 返回 返回

Jest單元測試 - 動態 詳情

由於格式和圖片解析問題,可前往 閲讀原文

前端自動化測試在提高代碼質量、減少錯誤、提高團隊協作和加速交付流程方面發揮着重要作用。它是現代軟件開發中不可或缺的一部分,可以幫助開發團隊構建可靠、高質量的應用程序

單元測試(Unit Testing)和端到端測試(End-to-End Testing)是兩種常見的測試方法,它們在測試的範圍、目的和執行方式上有所不同。單元測試和端到端測試不是相互排斥的,而是互補的。它們在不同的層面和階段提供了不同的價值,共同構成了一個全面的測試策略

單測和端測區別

單元測試(Unit)

  • 單元測試關注於最小的可測試單元,如函數、方法或模塊
  • 目的是驗證代碼中的每個獨立單元(如函數)是否按照預期工作
  • 通常是自動化的、快速執行的,且不依賴於外部資源或其他模塊
  • 驗證單個代碼單元的行為,提供快速反饋,並幫助捕獲和修復問題

端到端測試(End-to-End)

  • 從用户角度出發,測試整個應用程序的功能和流程
  • 模擬真實的用户交互和場景,從應用程序的外部進行測試。跨多個模塊、組件和服務進行,以確保整個應用程序的各個部分正常協同工作
  • 涉及用户界面(UI)交互、網絡請求、數據庫操作等,以驗證整個應用程序的功能和可用性

總之,單元測試主要關注代碼內部的正確性,而端到端測試關注整體功能和用户體驗。結合使用這兩種測試方法可以提高軟件的質量和可靠性。在項目中尤其是公共依賴如組件庫至少都需要單測,端測相對來説比較繁瑣點,但是也是程序穩定的重要驗證渠道

單元測試 - Jest

這裏使用Jest作為單元測試工具,Jest 是一個用於 JavaScript 應用程序的開源測試框架。它是由 Facebook 開發和維護的,通常用於單元測試。Jest 具有簡單易用的 API、豐富的功能和強大的斷言庫,廣泛應用於前端開發和 Node.js 環境中

安裝

➜ npm install jest -D

初始化

使用npx進行交互式生成默認的配置文件,它會提示你每步的選擇:

➜ npx jest --init
The following questions will help Jest to create a suitable configuration for your project

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … no
✔ Choose the test environment that will be used for testing › jsdom (browser-like)
✔ Do you want Jest to add coverage reports? … yes
✔ Which provider should be used to instrument code for coverage? › v8
✔ Automatically clear mock calls, instances, contexts and results before every test? … yes

✏️  Modified test/package.json

📝  Configuration file created at test/jest.config.js

默認配置文件大概是下面的內容:配置中有很多註釋提供我們參考,對於默認的配置就不用刪除多語的註釋了,方便參考。通常都是對需要的配置項做修改即可

const config = {
  // All imported modules in your tests should be mocked automatically
  // automock: false,

  // Automatically clear mock calls, instances, contexts and results before every test
  clearMocks: true,

  // Indicates whether the coverage information should be collected while executing the test
  collectCoverage: true,

  // An array of glob patterns indicating a set of files for which coverage information should be collected
  // collectCoverageFrom: undefined,

  // The directory where Jest should output its coverage files
  coverageDirectory: "coverage",

  // An array of regexp pattern strings used to skip coverage collection
  // coveragePathIgnorePatterns: [
  //   "/node_modules/"
  // ],

  // Indicates which provider should be used to instrument code for coverage
  coverageProvider: "v8",
  // Make calling deprecated APIs throw helpful error messages
  // errorOnDeprecated: false,

  // A list of paths to directories that Jest should use to search for files in
  // roots: [
  //   "<rootDir>"
  // ],

  // The test environment that will be used for testing
  testEnvironment: "jsdom",
  // 省略其他...
};

module.exports = config;

常用的配置:

  • collectCoverage:boolean值,用來生成覆蓋率報告,通常也可以使用命令行--coverage參數生成
  • moduleFileExtensions:對於引入文件可以省去文件後綴,jest會根據規則一一匹配
  • moduleNameMapper:模塊匹配規則,告訴jest改模塊的匹配路徑

    {
      moduleNameMapper: {
        // 當匹配到 .css 等結尾的文件時對應 /__mocks__/style-mock.ts 文件
        "\\.(css|less|scss|sass)$": "<rootDir>/__mocks__/style-mock.ts",
        // 當匹配 @ui 開頭的的對應到 src 文件夾
        "@ui/(.*)": "<rootDir>/src/$1",
      },
    }
  • setupFiles:在測試環境準備後和安裝jest框架前做一些配置,常用來添加一些全局環境模擬數據
  • setupFilesAfterEnv:在安裝jest框架後對jest做一些擴展,相比setupFiles更加通用
  • testEnvironment:jest模擬的環境,可以選擇node、jsdom來模擬node和瀏覽器環境
  • testMatch:指定要測試哪些文件
  • transform:使用一些插件對代碼進行轉義以便jest可以理解,如設置tsx轉義

以上是最基本的配置,jest的配置還是很多的,還要官方有列舉了一個表可以隨時翻閲不用死記

轉譯器

Jest中有轉義器的概念來幫助它理解編寫的代碼,可以比做babel對代碼做一些轉換來兼容瀏覽器,差不多一樣的道理

  1. 模塊引用轉換

    在單個測試文件中都會引入我們編寫的代碼,然後對此代碼的功能進行測試,而前端通常都是以esmodule的形式進行函數的導出,jest默認使用的是commonjs,對於module語法jest不認識就會報錯

    import { sum } from "../core"; // 報錯
    
    describe("第一個測試", () => {
        // ...
    })

    那麼可以對jest添加轉義器將esmodule模塊的代碼轉換成commonjs就可以了。打開配置文件:

    // jest.config.js
    {
      transform: {
        "^.+\\.(ts|tsx|js|jsx)$": [
          "babel-jest",
          {
            presets: [["@babel/preset-env", { targets: { node: "current" } }]]
          },
        ],
      },
    }

    上面使用了 babel-jest@babel/preset-env的依賴包需要安裝下:

    ➜ npm i babel-jest @babel/preset-env -D

    這樣就可以解決esmodule語法不識別的問題

  2. 轉換typescript:目前項目中的文件都是以ts編寫的,而默認情況下jest只識別js文件的,那麼就需要對ts進行轉譯讓jest識別

    // jest.config.js
    {
      transform: {
        "^.+\\.(ts|tsx|js|jsx)$": [
          "babel-jest",
          {
            presets: [/* 其它... */["@babel/preset-typescript"]]
          },
        ],
      },
    }

    需要安裝對應的@babel/preset-typescript;除了使用ts轉義器也可以使用ts-jest直接運行ts代碼

    得益於ts的轉譯插件可以讓jest的測試文件(或配置文件)都寫成ts類型的,而在ts文件中對於不識別的jest工具會報錯或者沒有提示,安裝jest的類型文件包@types/jest來告訴ts對應的jest類型,然後進行配置:

    // tsconfig.json
    {
      "types": ["jest"]
    }
  3. 轉換jsx:假如項目中使用了jsx那麼也要對jsx進行轉義,這裏以vue jsx為例

    // jest.config.ts
    {
      transform: {
        "^.+\\.(ts|tsx|js|jsx)$": [
          "babel-jest",
          {
            // 省略其他
            plugins: ["@vue/babel-plugin-jsx"],
          },
        ],
      },
    }

基本斷言

基本環境配置好後,就到了測試的時間了,我們先來最簡單的配置用起

// __tests__/demo.spec.ts
import { sum } from "src/utils";

describe("第一個測試", () => {
  it("分組1", () => {
    expect(sum(1, 2)).toBe(3);
  });
});

// 或者不用分組
test("第一個測試", () => {
  expect(sum(1, 2)).toBe(3);
});

這裏介紹下幾個關鍵字基本概念:

  • describe:用來描述當前測試的整體內容
  • it:用來分組測試
  • test:用來描述當前測試,無分組
  • expect:判斷參數的值,其的返回值有多個斷言方法,上面使用了toBe也就是等於的意思。除了次此斷言有很多斷言的條件,你可以點擊這裏閲讀官方文檔

執行測試

# 現在package中配置 jest 腳本,然後執行測試
➜ npm run test # npx jest
 PASS  __tests__/demo.spec.ts
  第一個測試
    ✓ 分組1 (2 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |     100 |      100 |     100 |     100 |                   
 utils.ts |     100 |      100 |     100 |     100 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.519 s
Ran all test suites.
✨  Done in 1.02s.

可以看到對應的測試文件、分組以及測試覆蓋率

路徑映射

上面在測試代碼時會先引入對應的工具代碼,如果都使用相對路徑引入會顯得很麻煩。在項目中通常都喜歡使用@這種方式引入文件,在測試環境依然可以使用,這樣也可以和項目中的文件路徑保持一致

配置路徑映射需要滿足兩個條件:

  1. jest識別路徑映射
  2. ts識別路徑映射(如果項目中用了ts)

配置jest路徑映射

// jest.config.ts
{
  moduleNameMapper: {
    "@/(.*)": "<rootDir>/src/$1",
  },
}

配置tsconfig

// tsconfig.json
{
  "paths": {
    "@/*": ["src/*"]
   }
}

這樣在測試文件中就可以使用路徑映射降低心智負擔

// __tests__/demo.spec.ts
import { sum } from "@/utils";

除了手動設置外還可以將tsconfig中的path直接作為路徑映射,這樣就減少了多處的修改。實現這一功能需要藉助ts-jest工具包,不同這個自己也可以寫個邏輯實現

// jest.config.ts
const { pathsToModuleNameMapper } = require('ts-jest/utils')
const { compilerOptions } = require('./tsconfig')

export default {
  moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, {
    prefix: "<rootDir>/",
  }),
}

dom測試

jest支持瀏覽器環境,使用瀏覽器環境時需要安裝對應的包,請根據版本可以選擇jsdomjest-environment-jsdom包進行安裝,這裏jest版本為28+使用後者。測試文件修改如下:

// __tests__/demo.spec.ts
describe("第一個測試", () => {
  it("分組1", () => {
    // 使用 localStorage API
    localStorage.setItem('a', '1');
    expect(localStorage.getItem(('a'))).toBe('1')
  });
});

運行測試用例:

➜ npm run test
PASS  __tests__/demo.spec.ts
  第一個測試
    ✓ 分組1 (2 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |       0 |        0 |       0 |       0 |                   
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.701 s, estimated 1 s
Ran all test suites.
✨  Done in 1.13s.

異步測試

jest可以使用多種方式進行異步代碼測試,通常使用promise、async就可以了

  1. 使用promise
  2. async/await
  3. 回調

這裏模擬一個異步方法,通過上面的三種方式進行測試

// src/utils
export function getUser(name: string) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(name), 1000);
  });
}

使用Promise

// __tests__/demo.spec.ts
import { getUser } from "@/utils";

describe("測試異步代碼", () => {
  it("promise調用方式測試", () => {
    const user = "小明";
    // 使用then
    getUser(user).then((res) => {
      expect(res).toBe(user);
    });
  });


  it("使用resolves測試promise", () => {
    const user = "小李";
    // 使用 .resolves 方式,注意這裏要 return
    return expect(getUser(user)).resolves.toBe(user);
  })
});

使用async測試

// __tests__/demo.spec.ts
import { getUser } from "@/utils";

describe("測試異步代碼", () => {
  it("使用async測試", async () => {
    const user = "小明";
    const res = await getUser(user)
    expect(res).toBe(user);
  })
});

使用回調函數

回調函數默認通常是以前那種回調寫法,這裏需要對以上的異步函數進行調整,讓其換成回調函數模式

// 接受一個cb,這裏固定返回的值為true,沒有錯誤
export function getUser(cb: (error: any, data: any) => void) {
  setTimeout(() => {
    cb(null, true);
  }, 500);
}

// 定義測試
describe("測試異步代碼", () => {
  it("使用回調函數", (done) => {
    function cb(error: any, data: any) {
      if (error) {
        done(error);
        return;
      }
      try {
        expect(data).toBe(true);
        done();
      } catch (err) {
        done(err); // 這裏一定要使用try catch,防止出錯時沒有執行done
      }
    }
    getUser(cb);
  });
});

回調模式一定要執行done函數,如果沒有執行則會被認為超時錯誤

模擬函數

假設要模擬一個工具函數的內部實現,可以使用mock函數來判斷函數內部的值是否達到預期

定義個待測試的函數forEach

export function forEach(items: number[], callback: (num: number) => void) {
  for (let index = 0; index < items.length; index++) {
    callback(items[index]);
  }
}

添加測試用例:

// __tests__/demo.spec.ts
import { forEach } from "@/utils";

// 模擬函數
const mockFn = jest.fn((x: number) => x + 1);

test("模擬函數", () => {
  forEach([0, 1], mockFn);

  expect(mockFn.mock.calls).toHaveLength(2);
  expect(mockFn.mock.calls[0][0]).toBe(0);
  expect(mockFn.mock.calls[1][0]).toBe(1);
  expect(mockFn.mock.results[0].value).toBe(1);
});

更多關於模擬函數的例子請查看文檔 和 API

定時器

Jest可以通過一個函數轉換計時器以便允許你控制時間流量

假設測試如下定時器代碼:

export function useTimer(cb?: Function) {
  setTimeout(() => cb && cb(), 1000);
}

編寫測試用例:

import { useTimer } from "@/utils";

jest.useFakeTimers();
jest.spyOn(global, "setTimeout");

test("test timer", () => {
  const cb = jest.fn();
  useTimer(cb);

  expect(cb).not.toBeCalled();
  
  // 執行所有的定時器
  jest.runAllTimers();

  expect(cb).toBeCalled();
});

更多見官方文檔

setup配置

寫測試的時候你經常需要在運行測試前做一些準備工作,和在運行測試後進行一些收尾工作。 Jest 提供輔助函數來處理這個問題

這其中包括beforeEach、afterEach、beforeAll、afterAll,其中前兩者在每個測試前都會執行一次,後者在文件中只會執行一次

覆蓋率

除了對程序進行斷言外,jest還收集代碼的測試覆蓋率並生成對應的報告,包括:某個函數內部的測試覆蓋率、整個文件的覆蓋率,要想達到覆蓋率100%,就要測試到每個文件的所有代碼、每個函數內部的所有分支條件

開啓覆蓋率

可以通過配置文件

// jest.config.ts
// 主要涉及到這兩個配置
export default {
  collectCoverage: true, // 啓用
  coverageDirectory: "coverage", // 報告生成位置
}

通過cli,執行腳本時帶上參數

➜ npx jest --coverage

測試覆蓋率

假設我們有這麼一個函數

export function whatType(arg: any) {
  const type = Object.prototype.toString.call(arg)
  if (type === '[object String]') {
    return 'string';
  } else if (type === '[object Boolean]') {
    return 'boolean';
  }
}

添加測試用例

import { whatType } from "@/utils";

describe("測試覆蓋率", () => {
  it("函數條件覆蓋率", () => {
    expect(whatType(true)).toBe("boolean");
  });
});

執行測試用例

➜ npm run test
 PASS  __tests__/demo.spec.ts
  測試覆蓋率
    ✓ 函數條件覆蓋率 (1 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |   96.77 |       50 |     100 |   96.77 |                   
 index.ts |   96.77 |       50 |     100 |   96.77 | 4                 
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.486 s, estimated 1 s
  • File:測試的文件
  • Stmts:測試中被執行的代碼語句的比例
  • Branch:測試代碼條件分支比例
  • Funcs:測試中被執行函數比例
  • Lines:測試中被執行代碼行數比例
  • Uncovered Line:沒有測試到的行數

除了查看終端的表格外,還可以使用更直觀的報告,文件報告的結構大概如下:

coverage
├── clover.xml # xml格式
├── coverage-final.json # json格式
├── lcov-report # html格式
│   ├── base.css
│   ├── block-navigation.js
│   ├── favicon.png
│   ├── index.html # 主頁面入口
│   ├── index.ts.html
│   ├── other.ts.html
│   ├── prettify.css
│   ├── prettify.js
│   ├── sort-arrow-sprite.png
│   └── sorter.js
└── lcov.info

一般都來查看HTML報告,打開報告頁面

可以點擊對應的文件查看更詳細的報告

Vue組件測試

jest也可以對vue組件進行測試,vue官方提供了 vue2版本工具包(vue-test) 和 vue3版本工具包(@vue/test-utils),這裏基於vue3組件進行測試

安裝對應的依賴:

➜ npm install @vue/test-utils -D

對於Jestv28+以上版本還需要添加以下配置:

// jest.config.ts
export default {
  testEnvironmentOptions: {
    customExportConditions: ["node", "node-addons"],
  },
}

創建一個簡單的Button組件:

import { defineComponent } from "vue";

export default defineComponent({
  render(){
    return <button>按鈕</button>
  }
})

添加測試用例:

import { mount } from "@vue/test-utils";
import Button from "@/components/Button";

test("測試vue組件", () => {
  const wrapper = mount({
    setup() {
      return () => {
        return <Button />;
      };
    },
  });
  expect(wrapper.text()).toBe('按鈕')
})

運行測試

➜ npm run test
 PASS  __tests__/demo.spec.tsx
  ✓ 測試vue組件 (9 ms)

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------|---------|----------|---------|---------|-------------------
All files   |     100 |      100 |     100 |     100 |                   
 Button.tsx |     100 |      100 |     100 |     100 |                   
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.633 s

添加全局組件,當單測某個組件時,組件內部引用的其它組件會因為沒有引用而報錯,定義全局組件可以解決這個問題

// jest.setup.ts
import { config } from "@vue/test-utils";
import Button from "@/button/src/button";
import Icon from "@/button/src/icon";

config.global.components = {
  Button,
  Icon,
};

配置jest

// jest.config.ts
export default {
  setupFiles: ["<rootDir>/jest.setup.ts"],
}

這裏不對vue工具包API過多的解釋,更多的API使用可以查看官方文檔,vue2版本的可以查看這裏

由於格式和圖片解析問題,可前往 閲讀原文
user avatar goustercloud 頭像 kobe_fans_zxc 頭像 xiaoxxuejishu 頭像 qishiwohendou 頭像 daqianduan 頭像 ccVue 頭像 DingyLand 頭像 gaozhipeng 頭像 laggage 頭像 yian 頭像 wnhyang 頭像 daishuyunshuzhanqianduan 頭像
點贊 48 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.