JavaScript Jest 測試

前端測試介紹及 Jest 基本應用

黃彥鈞 Jim Huang 2023/09/04 10:18:55
2866

在開發的時候,總是有各種理由不寫測試,像是開發時間太趕、寫測試太耗時間、寫測試好無聊等等,很多人覺得沒寫測試照樣能開發能上線,但真的是這樣嗎?本篇就來好好介紹前端測試的大小事。

 

為什麼要寫測試?

要做什麼事情,總是要有個說服人的原因,所以我列了幾點寫測試的優點:

  1. 確保程式碼正確,防止無預期的錯誤:像是改了 A 功能壞 B 功能。

  2. 方便重構程式碼:測試能確保重構後的功能依然是正確的。

  3. 交接容易:新人進入專案,能快速了解功能,且較不容易改壞原本的功能。

聽起來好處很多,那寫測試跟一般的人工測試有什麼不同呢?

以測試來說總共有分成三個種類:

  1. 單元測試 ( Unit Text ):針對程式中的最小單元進行測試,例如:Function、Class。

  2. 整合測試 ( Integration Test ):針對多個組件或模組之間的互動和整合測試,例如:使用者點擊按鈕跳出彈跳視窗。

  3. 端對端測試 ( End-to-End Test ):模擬真實使用者敬,測試整個應用程式的功能和流程,例如:使用者登入、使用者註冊。

大部分的人工測試都會歸類在端對端測試,單元測試及整合測試都會是在開發階段所進行。

而這三個種類又發展出一個測試金字塔的概念:

測試金字塔

  • 越靠近金字塔頂端(E2E 測試),所花費的成本越高,但是越貼近使用者

  • 越靠近金字塔底部(單元測試),所花費的成本越低,但是越貼近開發者

可以看到,進行越多的端對端測試,就會需要花更多的時間以及人力成本,像是寫測試報告以及人工測試,但是卻是最符合實際使用者的操作流程。所以大部分的專案都會盡量多做端對端測試,以確保使用者體驗。

但這也代表,如果今天有程式碼寫錯,可能只是很簡單的邏輯錯誤,但卻會導致花更大量的時間在反覆進行端對端測試。所以如果今天能夠做好單元測試及整合測試,就能防止花更多的時間在進行 E2E 測試。

 

JavaScript 測試框架

講了那麼多寫測試的好處,這就來介紹有哪些前端的 JavaScript 測試框架:

  1. Jest:由 FaceBook 所開發,在 React 專案上使用的人較多。

  2. Mocha:主要是 Node.js 的測試框架。

  3. Karma:Angular cli 預設搭配 Jasmine 作為測試框架。

  4. Vitest:近年來興起的測試框架,同 Vite 主打速度快。

這邊列了比較常見且較多人使用的測試框架,來做個詳細的比較:

compare

種類 Jest Mocha Karma Vitest
發布時間 2014 2011 2012 2020
開發者 FaceBook Open source Google Vite
下載量 1 2 3 4
是否內建斷言庫 ❌(需搭配 chi) ❌(需搭配 Jasmin)
是否含模擬函式 ❌(需搭配 Sinon.js) ❌(需搭配 Sinon.js)
跑測試速度 2 3 4 1
  • 斷言:判斷數入是否符合預期,Ex: expect(1+1).toBe(2)
  • 模擬函式:模擬元件或函式,Ex: jest.fn()

目前比較主流且最多人用的是 Jest,Mocha 及 Karma 沒有內建基本的斷言及模擬函式,且測試速度較慢,而 Vitest 測試速度最快,但發展還不成熟,可以持續觀望。

 

Jest 特色

  • 內建斷言庫

  • 內建模擬函式

  • 內建測試覆蓋率

  • 內建快照測試 

  • CRA 內建,Next.jsVite 也支援

基本語法

假設今天要測試一個加總的函式是否正確:

sum.js

const sum = (a, b) => {
  return a + b;
};
export default sum;

那測試代碼就會長這樣:

sum.test.js

import sum from './sum.js';

describe("sum function testing", () => {
  it("add 1 + 2 to equal 3", () => {
    expect(sum(1,2)).toBe(3);
  });
});
  • describe : 大範圍的測試描述

  • it / test : 詳細的測試案例描述

  • expect : 斷言

  • toBe : 斷言的方法 

在 Terminal  輸入:

jest sum.test.js

就會去跑有測試標記的檔案,並顯示測試結果:

除了基本語法外,Jest 還提供四個可以讓測試代碼更整潔的方法

  1. beforeAll : 在所有測試開始前執行

  2. beforeEach:在每個測試案例開始前執行

  3. afterEach:在每個測試案例結束後執行

  4. afterAll:在所有測試結束後執行

模擬函式

在測試中,需要把測試項目盡量單一化,所以會盡量把一些外部的引入的函式或模組進行模擬,比較常見的就是模擬發 API 回傳:

fetchData.js

import axios from 'axios';
import url from './url.js';

const fetchData = async() => {
  const response = await axios.get(url);
  return 'My content is:' + response.data;
}
export default fetchData

當我們要測試 fetchData 這個函式時,就需要去模擬 axios 回傳,只測試函式本身的功能,所以測試程式碼會是這樣:

fetchData.test.js

import fetchData from './fetchData.js'
import axios from 'axios';

jest.mock('axios'); //Mock axios

describe('test fetchData function',() => {
  it('call fetchData with "Hello" text get "My content is:Hello", () => {
    axios.get.mockResolvedValue({ data: 'Hello' }); // Mock response
    const res = fetchData();
    expect(res).toEqual('My data is:Hello)
  };
});

這邊需要先使用 jest.mock 去模擬 axios 套件,再使用 mockResolvedValue 去模擬 axios.get 函式回傳的 response,這樣就可以很單純的測試這個函式回傳是否正確,而不用實際的發 API 請求。

測試覆蓋率

Jest 有提供一個測試覆蓋率的功能,可以顯示有哪些程式碼有被測試到,只要下指令

jest --coverage

就可以在 Terminal 顯示測試覆蓋率

不過這樣看起來很不好看,所以 Jest 會同時建立一個 coverage 的資料夾,裡面會有一個 HTML 檔案,打開就會顯示轉換後的圖表

它總共有四個指標

  1. Statements : 有多少比例語句被執行到,一個  console.log(); 就算是一個語句,一行中可以有多個 Statements。

  2. Branches:條件語句,像是 if ... else 或是 switch,每個情況都是一個 Branch。

  3. Functions:一個檔案有多少比例的函式被執行到。

  4. Lines:有幾行的程式碼被執行到,基本上 Lines 的數量會小於 Statements 的數量

並不是需要所有指標都 100% 被覆蓋,最重要的就是 BranchesFunctions,屬於會影響功能邏輯的程式碼,能盡量提高測試覆蓋率是最好的。

快照測試 Snapshot Testing

快照就如同字面上的意思,就是快速的拍一張照,只是這一張照片是 DOM 元素的照片,用意是當 UI 有改變時,如果有建立快照,Jest 就可以很快的把當下的 DOM 跟快照做比較,找出有哪裡不一樣,這樣可以很快地抓到錯誤。

假設現在有一個函式會根據傳入的文字回傳一個 h1 的標題元素

getTitleHtml.js

const getTitleHtml = (text) => {
  return `<h1>${text}</h1>`;
}
export default getTitleHtml;

Jest 提供 toMatchSnapshot()斷言方法去比較快照。

getTitleHtml.test.js

import getTitleHtml from './getTitleHtml';
describe('testing snapshot', () => {
  it('match h1 element snapshot', () => {
    expect(getTitleHtml("Hello")).toMatchSnapshot();
  }
} 

跑一次測試會得到下面的結果

在第一次跑測試的時候,會在 __snapshots__ 檔案建立快照

裡面的內容會是長這樣

__snapshots__/getTitleHtml.test.js.snap

它是利用 describeit 的測試描述去記錄 DOM 的內容。

那再來測一次原本的函式,這次給他不一樣的內容:

import getTitleHtml from './getTitleHtml';
describe('testing snapshot', () => {
  it('match h1 element snapshot', () => {
    expect(getTitleHtml("Hello World !")).toMatchSnapshot(); // 更改 DOM 內容
  }
} 

再跑一次測試就會跑出錯誤,並且顯示哪邊不一樣。

如果想更新快照,只要在 watch  模式輸入 u (update),就會把快照的內容更新成現在的 DOM 內容。

 

總結

  1. 在開發時多寫單元測試整合測試是可以很有效的減少 E2E 的測試成本及時間。

  2. 以主流框架來說,Jest 是最多人使用且功能完善的。

  3. 大部分的 JavaScript 的測試框架語法都大同小異,所以只要學習某一個框架語法都是可以通用的。

  4. Jest 提供的測試覆蓋率可以有效的找出未測試的程式碼。

  5. 快照測試的用意比較像是提醒開發者 UI 有不一樣,所以通常會在確認畫面內容不會更動的情況下進行測試,不然會導致常常測試不通過。

 

參考資料 

Jest 官網

[Day 26] 快照測試(Snapshot Testing)是什麼?什麼時間適合使用?

 

黃彥鈞 Jim Huang