前端測試介紹及 Jest 基本應用
在開發的時候,總是有各種理由不寫測試,像是開發時間太趕、寫測試太耗時間、寫測試好無聊等等,很多人覺得沒寫測試照樣能開發能上線,但真的是這樣嗎?本篇就來好好介紹前端測試的大小事。
為什麼要寫測試?
要做什麼事情,總是要有個說服人的原因,所以我列了幾點寫測試的優點:
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 主打速度快。
這邊列了比較常見且較多人使用的測試框架,來做個詳細的比較:
種類 | Jest | Mocha | Karma | Vitest |
發布時間 | 2014 | 2011 | 2012 | 2020 |
開發者 | Open source | 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 特色
基本語法
假設今天要測試一個加總的函式是否正確:
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% 被覆蓋,最重要的就是 Branches 跟 Functions,屬於會影響功能邏輯的程式碼,能盡量提高測試覆蓋率是最好的。
快照測試 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
它是利用 describe
跟 it
的測試描述去記錄 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 有不一樣,所以通常會在確認畫面內容不會更動的情況下進行測試,不然會導致常常測試不通過。
參考資料
[Day 26] 快照測試(Snapshot Testing)是什麼?什麼時間適合使用?