捨棄 create-react-app 之餘還架了個 astro blog 昭告天下:單元測試

寫測試可以幫助看 code 的人(可能是別人、也可能是未來的自己)明確暸解一個功能究竟能接受哪些參數、又會有哪幾種可能的輸出內容,也保障一個功能在「單元測試有覆蓋到的部分」是有品質保證的。為了能在晚上睡得更安穩,能從元件中獨立出來的邏輯就盡量提供配合的單元測試吧。

於是今天的主題就是:如何為白手起家的 React TypeScript app 專案設定 jest ( つ•̀ω•́)つ

安裝套件

在寫這篇鐵人賽文章的當下 jest 版本為 29.6,為了在這版 jest 順利把 TypeScript 測試跑起來,你需要安裝以下套件:

yarn add -D @jest/globals jest ts-jest ts-node

除了 jest 以外都是輔助 TypeScript 用的套件。如果在你閱讀這篇文章時 jest 的版本已遠超 29.6,建議在安裝完 jest 後,根據執行 yarn jest 後的終端訊息一個一個把需要的套件裝回去。

設定 jest.config.ts

在專案根目錄新增一個 jest.config.ts 檔案並填入以下內容:

import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom', // 選填,見下方說明
  moduleNameMapper: {
    '@/jest.config': '<rootDir>/jest.config.ts',
    '@Model/(.*)': '<rootDir>/src/model/$1',
    '@Tool/(.*)': '<rootDir>/src/tool/$1',
  },
};

export default config;

解說如下:

在執行 .ts 類型的單元測試時,以上的設定基本上就足夠了。

範例測試

首先在 ./src/tool 中新增了一個檔案 isValidUser.ts 並撰寫以下功能:

export function isValidUser(arg: unknown) {
  return (
    typeof arg === 'object' && !!arg && 'userName' in arg && !!arg.userName
  );
}

接著在 ./src/tool 中新增 isValidUser.test.ts 來撰寫測試腳本:

import { describe, expect, test } from '@jest/globals';
import { isValidUser } from '@Tool/isValidUser';

describe('isValidUser', () => {
  test('should return false if the argument is not an object', () => {
    expect(isValidUser(null)).toBe(false);
    expect(isValidUser(undefined)).toBe(false);
    expect(isValidUser(123)).toBe(false);
    expect(isValidUser('abc')).toBe(false);
    expect(isValidUser(true)).toBe(false);
    expect(isValidUser(false)).toBe(false);
  });
  test('should return false if the argument is an empty object', () => {
    expect(isValidUser({})).toBe(false);
  });
  test('should return false if the argument is an object without userName', () => {
    expect(isValidUser({ name: 'user A' })).toBe(false);
  });
  test('should return false if the argument is an object with empty userName', () => {
    expect(isValidUser({ userName: '' })).toBe(false);
  });
  test('should return true if the argument is an object with non-empty userName', () => {
    expect(isValidUser({ userName: 'user A' })).toBe(true);
  });
});

(純個人感受:GitHub Copilot 為簡單功能產生測試腳本的表現真的不錯,個人版方案一年 100 美金實在不算太貴,可以考慮課個金)

開啟終端,輸入 make test(參考第 6 天)即可執行單元測試:

 PASS  src/tool/isValidUser.test.ts
  isValidUser
 should return false if the argument is not an object (4 ms)
 should return false if the argument is an empty object
 should return false if the argument is an object without userName (1 ms)
 should return false if the argument is an object with empty userName
 should return true if the argument is an object with non-empty userName

單元測試的哲學

個人認為 Good Code, Bad Code 一書第十章「單元測試準則」對測試做了不錯的總結。目前在撰寫單元測試時,我會盡可能讓測試腳本都能符合以下特徵:

  1. 測試要能確實反映問題,且也不會產生誤報
  2. 以測試行為為主,不過份涉入實作細節
  3. 測試失敗時,應能提供具體的錯誤說明(發生在哪裡、哪一個項目失敗、收到的非預期結果又是什麼)
  4. 當其他工程師閱讀測試內容時,能夠理解「被測試的功能在做什麼」
  5. 容易被執行,比如透過終端即可觸發、不需要在每次測試時都需要大費周章地進行前置作業

附上個人之前的筆記,有較為細節的摘要,歡迎參考。

總結

對 TypeScript React app 專案設定單元測試的門檻並不高,多一點保險可以改善工程師的睡眠品質,真心推薦。