name: bdd-unit-test description: > 指定一或多個檔案並要求撰寫單元測試時,必須載入此技能。 根據 BDD(行為驅動開發)原則,分析指定檔案的公開方法與邏輯分支,產出涵蓋 Happy Path、Edge Cases、Error Cases 的測試。 觸發情境包含但不限於:「幫這個檔案寫單測」、「write unit test」、「add unit test」。 即使只說「寫測試」或「補測試」,只要有指定目標檔案或貼上程式碼片段,也應載入此技能。
單元測試撰寫指南(BDD 原則)
執行流程
- 判別語言:根據檔案副檔名判別程式語言,載入對應範例
- 分析原始碼:讀取一或多個指定檔案,識別所有公開方法、條件分支、邊界情況
- 列出測試場景:使用 BDD 格式列出所有需測試的場景(先輸出給用戶確認)
- 撰寫測試:根據確認的場景撰寫測試程式碼
- 輸出測試檔案:依據語言規範輸出到正確位置
測試範圍
✅ 包含
- 純邏輯函數(計算、驗證、轉換)
- 服務層方法(business logic)
- 工具函數(utils/helpers)
- 資料處理(parsing、formatting)
- 狀態管理邏輯(非 UI 綁定部分)
❌ 不包含
- UI 畫面測試:不測試 React/Vue 元件的渲染結果、DOM 結構
- 純展示元件:若元件只負責靜態文字、圖片或樣式呈現,沒有業務邏輯、條件分支、狀態轉換或使用者互動,不需要撰寫單元測試
- CSS class 驗證:不使用
querySelector、querySelectorAll、container.querySelector或其他 CSS selector 抓取 class、DOM 節點來驗證畫面 - 視覺回歸測試:不測試樣式、佈局、截圖比對
- E2E 測試:不測試完整使用者流程、瀏覽器互動
- 整合測試:不測試多個模組的整合行為
UI 測試穩定性原則
- 測試應驗證可觀察的功能行為,不要綁定容易因重構或樣式調整而變動的 CSS class、DOM 階層或 selector。
- 禁止為了取得元素而使用
querySelector、querySelectorAll、container.querySelector,以及以.className、[class*=...]等 CSS selector 進行驗證。 - 若元件確實包含互動或條件行為,優先使用可存取角色、名稱、標籤、文字或測試框架提供的語意查詢,例如 Testing Library 的
getByRole、getByLabelText、getByText。 - 不驗證實作細節,例如 class 名稱、內部 DOM 包裝層數或純樣式切換;只驗證使用者可感知的結果與業務行為。
- 分析目標檔案後若確認只是簡單畫面展示元件,場景分析應明確標示「不需要單元測試」並停止產生測試檔案。
語言判別規則
根據目標檔案的副檔名判別語言,分兩條路徑處理:
已知語言(有 reference 範例)
| 副檔名 | 語言 | 載入範例 |
|---|---|---|
.ts, .tsx |
TypeScript | references/typescript-example.test.ts |
.js, .jsx |
JavaScript | references/typescript-example.test.ts |
.cs |
C# | references/csharp-example-test.cs |
.java |
Java | references/java-example-test.java |
.py |
Python | references/python-example-test.py |
.dart |
Flutter/Dart | references/flutter-example.dart |
執行步驟:
- 取得目標檔案的副檔名
- 根據上表判別語言
- 讀取對應的 example 檔案作為撰寫測試的參考
- 依照該語言的規範(框架、命名、輸出位置)產出測試
未知語言(無 reference 範例,fallback)
副檔名不在上表時,直接以 LLM 通用知識處理:
- 根據副檔名推斷語言與該語言主流測試框架(如
.dart→flutter_test、.rb→ RSpec、.go→testing套件) - 套用相同的 BDD 原則、Given/When/Then 命名、場景分類(Happy / Edge / Error)
- 依該語言慣例決定檔案命名與輸出位置
- 不讀取任何 reference 檔案,不需告知使用者
BDD 核心原則
Given-When-Then 結構
Given [前置條件/初始狀態]
When [執行的動作/觸發事件]
Then [預期結果/驗證行為]
測試場景分類(必須涵蓋)
| 分類 | 說明 | 範例 |
|---|---|---|
| ✅ Happy Path | 正常流程、預期輸入 | 有效參數、正確格式 |
| ⚠️ Edge Cases | 邊界條件 | 空值、最大/最小值、零 |
| ❌ Error Cases | 異常處理 | 無效輸入、例外拋出 |
| 🔄 State Changes | 狀態轉換 | 初始化、重置、更新 |
測試命名規範
採用 Given條件_When動作_Should預期行為 格式:
GivenNoItems_WhenGetList_ShouldReturnEmptyList
GivenNullInput_WhenValidate_ShouldThrowException
GivenItemsExist_WhenCalculateTotal_ShouldReturnCorrectSum
語言別測試規範
🟨 JavaScript/TypeScript (.ts, .tsx)
| 項目 | 規範 |
|---|---|
| 框架 | Jest |
| 檔案命名 | [ComponentName].test.tsx |
| 輸出位置 | 同目錄的 __tests__/ 資料夾 |
| Mock 工具 | jest.mock() |
| 斷言風格 | expect(result).toBe(expected) |
結構範例:
describe('模組/元件名稱', () => {
describe('方法名稱', () => {
it('Given[條件]_When[動作]_Should[預期行為]', () => {
// Given - 準備測試資料
const input = { ... };
// When - 執行被測方法
const result = targetMethod(input);
// Then - 驗證結果
expect(result).toEqual(expected);
});
});
});
🟦 C# (.cs)
| 項目 | 規範 |
|---|---|
| 框架 | xUnit |
| 檔案命名 | [ClassName]Test.cs |
| 輸出位置 | 對應的 .Tests 專案資料夾 |
| Mock 工具 | Moq |
| 斷言風格 | Assert.Equal(expected, actual) |
結構範例:
public class UserServiceTests
{
[Fact]
public void GivenUserExists_WhenGetUser_ShouldReturnUser()
{
// Given
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(r => r.GetById(1)).Returns(new User { Id = 1 });
var service = new UserService(mockRepo.Object);
// When
var result = service.GetUser(1);
// Then
Assert.NotNull(result);
Assert.Equal(1, result.Id);
}
}
☕ Java (.java)
| 項目 | 規範 |
|---|---|
| 框架 | JUnit 5 / TestNG |
| 檔案命名 | [ClassName]Test.java |
| 輸出位置 | src/test/java/ 對應套件路徑 |
| Mock 工具 | Mockito, MockK (Kotlin) |
| 斷言風格 | AssertJ: assertThat(result).isEqualTo(expected) |
結構範例:
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@Test
@DisplayName("應該在訂單存在時返回訂單")
void GivenOrderExists_WhenGetOrder_ShouldReturnOrder() {
// Given - 設定 mock 行為
Order expected = new Order(1L, "item");
when(orderRepository.findById(1L)).thenReturn(Optional.of(expected));
// When - 執行被測方法
Order result = orderService.getOrder(1L);
// Then - 驗證結果
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(1L);
}
}
🐍 Python (.py)
| 項目 | 規範 |
|---|---|
| 框架 | pytest / unittest |
| 檔案命名 | test_[module_name].py |
| 輸出位置 | tests/ 資料夾,保持與 src 相同結構 |
| Mock 工具 | unittest.mock, pytest-mock |
| 斷言風格 | assert result == expected |
結構範例:
import pytest
from unittest.mock import Mock, patch
class TestUserService:
"""UserService 單元測試"""
def test_GivenUserExists_WhenGetUser_ShouldReturnUserData(self):
"""應該在用戶存在時返回用戶資料"""
# Given
mock_repo = Mock()
mock_repo.get_by_id.return_value = {"id": 1, "name": "Test"}
service = UserService(mock_repo)
# When
result = service.get_user(1)
# Then
assert result is not None
assert result["id"] == 1
🐦 Flutter/Dart (.dart)
| 項目 | 規範 |
|---|---|
| 框架 | flutter_test + mocktail |
| 檔案命名 | [class_name]_test.dart |
| 輸出位置 | test/ 資料夾,保持與 lib 相同結構 |
| Mock 工具 | mocktail(class MockX extends Mock implements X {}) |
| 斷言風格 | expect(result, equals(expected)) |
結構範例:
void main() {
late CartService cartService;
late MockProductRepository mockProductRepo;
setUp(() {
mockProductRepo = MockProductRepository();
cartService = CartService(mockProductRepo);
});
group('CartService', () {
group('addItem', () {
test('GivenProductExists_WhenAddItem_ShouldAddToCart', () {
// Given
when(() => mockProductRepo.findById(1)).thenReturn(product);
// When
final result = cartService.addItem(1, 2);
// Then
expect(result.items, hasLength(1));
});
});
});
}
Mock 使用原則
何時使用 Mock
- 📦 外部套件依賴(npm packages、第三方函式庫)
- 🌐 外部 API 呼叫(HTTP requests)
- 🗄️ 資料庫操作
- 📁 檔案系統存取
- ⏰ 時間相關函數(Date.now, datetime)
- 🎲 隨機數生成
Mock 最佳實踐
- 只 Mock 直接依賴:不要 Mock 被測單元的內部實作
- 驗證互動:確認 Mock 被正確呼叫(次數、參數)
- 重置狀態:每個測試後清理 Mock 狀態
- 避免過度 Mock:過多 Mock 可能表示耦合度太高
輸出格式
執行此 skill 時,請依序輸出:
1️⃣ 場景分析
## 📋 測試場景分析
**目標檔案:** `path/to/file.ts`, `path/to/other.ts`(單檔時僅列一個)
**識別的公開方法:** methodA, methodB, methodC
### 測試場景列表
| # | 方法 | 場景類型 | 描述 |
|---|------|----------|------|
| 1 | methodA | ✅ Happy | 當輸入有效時應返回正確結果 |
| 2 | methodA | ⚠️ Edge | 當輸入為空時應返回空陣列 |
| 3 | methodA | ❌ Error | 當輸入為 null 時應拋出例外 |
確認以上場景後,我將開始撰寫測試。
2️⃣ 測試程式碼
依據確認的場景,輸出完整測試檔案,包含:
- 必要的 import
- 完整的測試結構
- 每個測試的 Given/When/Then 註解
參考範例
根據判別的語言,讀取對應的範例檔案:
- TypeScript/JavaScript:
references/typescript-example.test.ts - C#:
references/csharp-example-test.cs - Java:
references/java-example-test.java - Python:
references/python-example-test.py - Flutter/Dart:
references/flutter-example.dart
注意:撰寫測試前必須先讀取對應語言的範例,確保遵循一致的風格與結構。