spec-kit 入門解說:從零開始建立你的第一個測試套件
引言/概述
在現代軟體開發中,測試扮演著至關重要的角色。它不僅能確保程式碼的品質和可靠性,還能為開發者提供修改和重構的信心,並作為程式碼行為的活文件。然而,對於許多剛接觸測試的開發者來說,入門可能會感到有些卻步。
本教學文章旨在為您提供一個循序漸進的 spec-kit 入門指南。我們將從基礎概念開始,逐步帶您完成環境設定、撰寫第一個測試、執行測試,並介紹一些進階的測試技巧和最佳實踐。為了提供具體的實作範例,我們將以廣受歡迎的 JavaScript 測試框架 Jest 作為 spec-kit 概念的實踐工具。透過本文,您將能夠掌握軟體測試的基本流程,並將其應用於您的專案中。
什麼是 spec-kit?為什麼要使用它?
什麼是 spec-kit?
spec-kit 並非指單一特定的測試框架,而是一個用於描述、組織和執行測試的「規範工具包」的通用概念。它通常包含以下核心要素:
- 測試套件 (Test Suites):用於將相關測試分組,提供清晰的結構。
- 測試案例 (Test Cases):定義單一、具體的行為或功能測試。
- 斷言 (Assertions):用於驗證程式碼的輸出是否符合預期。
- 比對器 (Matchers):提供多種方便的語法來進行斷言比對。
- 測試生命週期鉤子 (Lifecycle Hooks):允許在測試執行前後設定或清理環境。
在本教學中,我們將使用 Jest 來具體實踐這些 spec-kit 概念。Jest 是由 Facebook 開發的一個功能豐富、易於使用的 JavaScript 測試框架,它提供了上述所有功能,並且開箱即用,非常適合入門學習。
為什麼要使用 spec-kit (或 Jest)?
採用測試框架如 Jest 進行開發,能為您的專案帶來多方面的好處:
- 提升程式碼品質與穩定性:測試可以捕捉到程式碼中的錯誤和缺陷,確保每個功能都按照預期工作,從而交付更穩定、高品質的產品。
- 促進重構與維護:當您需要修改或優化現有程式碼時,完善的測試套件能作為安全網。只要測試全部通過,您就可以確信更改沒有引入新的錯誤,讓重構變得更安心、更有效率。
- 作為活文件:測試案例本身就是對程式碼行為的具體描述。它們清晰地展示了程式碼在不同輸入下的預期輸出,這對於新成員理解專案或老成員回顧功能都非常有幫助。
- 快速回饋循環:自動化測試可以在每次程式碼更改後快速執行,立即發現問題,縮短了調試和修正錯誤的時間,加速開發流程。
- 改善團隊協作:清晰的測試規範有助於團隊成員之間更好地理解和溝通程式碼的預期行為,減少誤解。
環境設定與安裝
在開始撰寫測試之前,我們需要先設定開發環境。
前置條件
您需要確保系統上已安裝 Node.js 和 npm(或 yarn)。您可以透過以下指令檢查它們的版本:
node -v
npm -v
# 或
yarn -v
如果未安裝,請前往 Node.js 官方網站 (https://nodejs.org/) 下載並安裝。
初始化專案
首先,建立一個新的專案資料夾,並在其中初始化一個 Node.js 專案:
mkdir my-spec-kit-project
cd my-spec-kit-project
npm init -y
# 或
yarn init -y
npm init -y 會使用預設值快速建立一個 package.json 檔案。
安裝 spec-kit (使用 Jest 作為實作範例)
現在,我們將安裝 Jest 作為開發依賴項:
npm install --save-dev jest
# 或
yarn add --dev jest
設定 package.json
安裝完成後,開啟您的 package.json 檔案。我們需要新增一個 test 腳本,這樣就可以透過簡單的指令來執行測試:
{
"name": "my-spec-kit-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest" // 新增這一行
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^29.x.x" // 您的 Jest 版本可能不同
}
}
現在,您的環境已經準備就緒,可以開始撰寫測試了!
spec-kit 核心概念
在使用 Jest 撰寫測試時,您會頻繁接觸到以下幾個核心概念。
測試檔案結構
Jest 預設會尋找專案中以下模式的檔案作為測試:
* 所有在 __tests__ 資料夾下的 .js, .jsx, .ts, .tsx 檔案。
* 檔名為 .test.js 或 .spec.js (或其 TypeScript 版本) 的檔案。
通常,您可以將測試檔案放在與被測試程式碼相同的資料夾中,例如 my-module.js 和 my-module.test.js,或者將所有測試集中在一個 __tests__ 資料夾中。
describe:測試套件
describe 函數用於將一組相關的測試案例組織成一個「測試套件」。它接受兩個參數:
1. 名稱 (string):一個描述性的字串,說明這個測試套件的目的。
2. 回呼函數 (function):包含實際測試案例 (it) 和其他 describe 區塊。
describe('我的第一個測試套件', () => {
// 這裡可以包含多個 it 測試案例
// 或其他嵌套的 describe 區塊
});
it (或 test):單一測試案例
it 函數 (Jest 也提供 test 作為別名) 用於定義一個單一的測試案例。每個 it 區塊應該測試程式碼的一個特定行為或功能。它也接受兩個參數:
1. 名稱 (string):一個清晰的字串,說明這個測試案例預期會做什麼。
2. 回呼函數 (function):包含實際的測試邏輯,通常是呼叫被測試的程式碼並進行斷言。
describe('數學運算', () => {
it('應該正確地將兩個數字相加', () => {
// 測試邏輯和斷言
});
it('應該正確地處理負數相加', () => {
// 測試邏輯和斷言
});
});
expect:斷言
expect 函數是 Jest 中進行斷言的核心。它接受一個值作為參數,這個值就是您想要測試的「實際值」。expect 的回傳值是一個「比對器物件」,您可以在其上鏈接各種比對器。
expect(實際值) // 返回一個比對器物件
.比對器(預期值); // 呼叫比對器方法
Matchers:比對器
比對器是 expect 物件上的方法,用於將實際值與預期值進行比較。Jest 提供了豐富的比對器,以下是一些常用的:
| 比對器 | 說明 | 範例 |
|---|---|---|
toBe(expected) |
檢查嚴格相等 (===) | expect(1).toBe(1); |
toEqual(expected) |
檢查值相等 (遞迴比較物件或陣列的內容) | expect({a:1}).toEqual({a:1}); |
not |
反轉下一個比對器的結果 | expect(1).not.toBe(2); |
toBeTruthy() |
檢查值是否為真值 (truthy) | expect(true).toBeTruthy(); |
toBeFalsy() |
檢查值是否為假值 (falsy) | expect(false).toBeFalsy(); |
toBeNull() |
檢查值是否為 null |
expect(null).toBeNull(); |
toBeUndefined() |
檢查值是否為 undefined |
expect(undefined).toBeUndefined(); |
toBeDefined() |
檢查值是否不是 undefined |
expect(0).toBeDefined(); |
toContain(item) |
檢查陣列是否包含特定項目 | expect([1, 2, 3]).toContain(2); |
toHaveLength(number) |
檢查陣列或字串的長度 | expect([1, 2]).toHaveLength(2); |
toMatch(regexpOrString) |
檢查字串是否匹配正則表達式或子字串 | expect('hello').toMatch(/ell/); |
toThrow(error) |
檢查函數是否拋出錯誤 | expect(() => throwError()).toThrow(); |
toBeGreaterThan(number) |
檢查數值是否大於 | expect(10).toBeGreaterThan(5); |
toBeLessThan(number) |
檢查數值是否小於 | expect(5).toBeLessThan(10); |
toBeCloseTo(number, precision) |
檢查浮點數是否接近某個值 (考慮精度) | expect(0.1 + 0.2).toBeCloseTo(0.3); |
撰寫你的第一個 spec-kit 測試
現在,讓我們來撰寫一個簡單的函數,並為它建立第一個測試套件。
建立待測試的程式碼
在專案根目錄下建立一個 src 資料夾,並在其中建立一個 add.js 檔案:
src/add.js
// 一個簡單的加法函數
function add(a, b) {
return a + b;
}
module.exports = add; // 導出函數以便在測試中引入
建立測試檔案
在專案根目錄下建立一個 __tests__ 資料夾,並在其中建立一個 add.test.js 檔案:
__tests__/add.test.js
// 引入我們想要測試的 add 函數
const add = require('../src/add');
// 使用 describe 定義一個測試套件,用於測試 add 函數
describe('add 函數', () => {
// 第一個測試案例:測試兩個正數相加
it('應該正確地將兩個正數相加', () => {
// 安排 (Arrange): 準備輸入
const num1 = 1;
const num2 = 2;
// 執行 (Act): 呼叫被測試的函數
const result = add(num1, num2);
// 斷言 (Assert): 驗證結果是否符合預期
expect(result).toBe(3); // 預期 1 + 2 等於 3
});
// 第二個測試案例:測試正數與負數相加
it('應該正確地將正數與負數相加', () => {
expect(add(5, -3)).toBe(2); // 預期 5 + (-3) 等於 2
});
// 第三個測試案例:測試零的處理
it('應該正確地處理零的加法', () => {
expect(add(0, 7)).toBe(7);
expect(add(7, 0)).toBe(7);
expect(add(0, 0)).toBe(0);
});
// 第四個測試案例:測試浮點數相加 (注意浮點數精度問題)
it('應該處理浮點數相加 (使用 toBeCloseTo)', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3); // 浮點數比較建議使用 toBeCloseTo
});
});
在上面的範例中,我們遵循了測試撰寫的 A-A-A 模式:
* Arrange (準備):設定測試所需的條件和輸入(例如 const num1 = 1;)。
* Act (執行):執行被測試的程式碼(例如 const result = add(num1, num2);)。
* Assert (斷言):驗證結果是否符合預期(例如 expect(result).toBe(3);)。
執行測試與查看結果
完成測試檔案後,您就可以執行它們了。
執行指令
在終端機中,切換到您的專案根目錄,然後執行:
npm test
# 或
yarn test
解讀測試報告
執行指令後,Jest 會掃描您的測試檔案,執行所有測試案例,並在終端機中顯示詳細的測試報告。
成功執行的輸出範例:
PASS __tests__/add.test.js
add 函數
✓ 應該正確地將兩個正數相加 (2 ms)
✓ 應該正確地將正數與負數相加 (1 ms)
✓ 應該正確地處理零的加法 (1 ms)
✓ 應該處理浮點數相加 (使用 toBeCloseTo) (0 ms)
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 1.234 s
Ran all test suites.
如果您的測試失敗,Jest 會提供清晰的錯誤訊息,指示哪個測試案例失敗、預期值和實際值是什麼,以及失敗發生的程式碼位置。這對於快速定位和修復問題非常有幫助。
進階概念:測試生命週期與非同步測試
測試生命週期鉤子
Jest 提供了多個生命週期鉤子,讓您可以在測試套件或每個測試案例執行前後執行設定 (setup) 或清理 (teardown) 的程式碼。
| 鉤子 | 說明 |
|---|---|
beforeAll(fn) |
在當前 describe 區塊中的所有測試之前執行一次。 |
afterAll(fn) |
在當前 describe 區塊中的所有測試之後執行一次。 |
beforeEach(fn) |
在當前 describe 區塊中的每個 it 測試之前執行。 |
afterEach(fn) |
在當前 describe 區塊中的每個 it 測試之後執行。 |
這些鉤子在需要初始化資料庫連接、設定模擬伺服器、或清理臨時文件等場景中非常有用。
範例:使用生命週期鉤子
// __tests__/lifecycle.test.js
let sharedResource = 0; // 假設這是一個共享資源
describe('共享資源操作', () => {
// 在此 describe 區塊中的所有測試開始前執行一次
beforeAll(() => {
console.log('--- beforeAll: 初始化共享資源 ---');
// 模擬初始化一個昂貴的資源
sharedResource = 100;
});
// 在此 describe 區塊中的每個測試案例執行前執行
beforeEach(() => {
console.log(' beforeEach: 重置共享資源為初始值');
sharedResource = 0; // 確保每個測試案例的環境是乾淨的
});
// 在此 describe 區塊中的每個測試案例執行後執行
afterEach(() => {
console.log(' afterEach: 清理測試後狀態');
// 可以在這裡清理每個測試案例留下的痕跡
});
// 在此 describe 區塊中的所有測試結束後執行一次
afterAll(() => {
console.log('--- afterAll: 釋放共享資源 ---');
sharedResource = null; // 模擬釋放資源
});
it('應該能增加共享資源的值', () => {
console.log(' 執行測試 1: 增加資源');
sharedResource++;
expect(sharedResource).toBe(1); // beforeEach 設置為 0,然後增加 1
});
it('應該能減少共享資源的值', () => {
console.log(' 執行測試 2: 減少資源');
sharedResource--;
expect(sharedResource).toBe(-1); // beforeEach 設置為 0,然後減少 1
});
});
非同步測試
現代 JavaScript 應用程式大量依賴非同步操作(例如 API 請求、定時器)。Jest 提供了多種方式來測試這些非同步程式碼。
-
使用
done回呼:這是傳統的方式。當您的非同步操作完成時,呼叫done()來告訴 Jest 測試已結束。javascript
it('應該在非同步操作完成後返回數據 (done)', (done) => {
setTimeout(() => {
expect(1).toBe(1);
done(); // 必須呼叫 done(),否則測試會超時
}, 100);
}); -
返回一個 Promise:如果您的非同步函數返回一個 Promise,您可以直接在
it函數中返回它。Jest 會等待 Promise 解決或拒絕。```javascript
function fetchData() {
return Promise.resolve('some data');
}it('應該在 Promise 解決後返回數據', () => {
return fetchData().then(data => {
expect(data).toBe('some data');
});
});// 更簡潔的方式,使用 .resolves 比對器
it('應該使用 .resolves 比對器檢查 Promise 解決值', () => {
return expect(fetchData()).resolves.toBe('some data');
});
``` -
使用
async/await:這是測試 Promise 最現代且推薦的方式。``javascriptUser ${id}` }), 50));
async function fetchUserData(id) {
return new Promise(resolve => setTimeout(() => resolve({ id: id, name:
}it('應該使用 async/await 獲取用戶數據', async () => {
const user = await fetchUserData(1);
expect(user).toEqual({ id: 1, name: 'User 1' });
});it('應該處理非同步錯誤', async () => {
// 假設 fetchUserData 在某些情況下會拋出錯誤
await expect(Promise.reject(new Error('Network error'))).rejects.toThrow('Network error');
});
```
測試最佳實踐
遵循一些最佳實踐可以讓您的測試套件更有效、更易於維護:
- 單一職責原則 (Single Responsibility Principle):每個
it測試案例應該只測試程式碼的一個特定行為或功能。這使得測試失敗時更容易找出問題根源。 - A-A-A 模式:清楚地將測試分為 Arrange (準備)、Act (執行)、Assert (斷言) 三個階段,提高測試的可讀性。
- 測試隔離:確保測試案例之間相互獨立,一個測試的結果不應影響其他測試。使用
beforeEach和afterEach來設定和清理環境是實現隔離的關鍵。 - 描述性測試名稱:
describe和it的名稱應該清晰、簡潔地描述測試的目的,即使不看程式碼也能理解。例如,'應該正確地將兩個正數相加'優於'測試加法'。 - 快速執行:單元測試應該盡可能快地執行,以便在開發過程中提供即時回饋。避免在單元測試中進行真實的網路請求或資料庫操作(可以使用 Mock 或 Stub 代替)。
- 只測試公共接口:通常只測試模組或組件的公共 API,而不是內部實現細節。當內部實現改變時,只要公共行為不變,測試就不應該失敗。
- 程式碼覆蓋率 (Code Coverage):雖然不是唯一指標,但追蹤程式碼覆蓋率可以幫助您了解哪些部分尚未被測試到。Jest 內建了覆蓋率報告功能。
總結
透過本教學,您已經學習了 spec-kit 的核心概念,並使用 Jest 成功建立了第一個測試套件。您了解了 describe 用於組織測試,it 定義單一測試案例,以及 expect 配合各種 Matchers 進行斷言。此外,我們還探討了生命週期鉤子和非同步測試的處理方式,以及一些重要的測試最佳實踐。
將測試融入您的開發流程,不僅能顯著提高程式碼品質,還能讓您在面對複雜性時更有信心。從現在開始,嘗試為您撰寫的每個新功能和修復的每個 Bug 撰寫測試。隨著經驗的累積,您將會發現測試是軟體開發中不可或缺且極具價值的工具。
祝您測試愉快!