iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Software Development

重啟挑戰:老派軟體工程師的測試修練系列 第 27

Day 27 – GitHub Copilot 測試實戰:AI 輔助測試開發指南

  • 分享至 

  • xImage
  •  

在這個 AI 革命的時代,我們如何讓人工智慧成為測試開發的最佳夥伴?

前言

每當我們坐在電腦前準備寫測試時,是否曾經有過這樣的想法:「又要寫重複的測試程式碼了」、「這個邊界條件我可能會忘記測試」、「Mock 物件的設定總是讓我頭痛」?

傳統的測試開發確實充滿挑戰:測試案例的重複性高、邊界條件容易遺漏、Mock 物件設定繁瑣、新手往往需要較長時間才能掌握。但現在,GitHub Copilot 的出現為我們帶來了全新的解決方案。

今天我們將深入探討如何善用 GitHub Copilot 來革新測試開發流程,從基礎設定到進階應用,打造一個高效的測試開發環境。

GitHub Copilot 測試應用概述

AI 輔助測試開發的四大優勢

效率提升:從重複勞動中解放

GitHub Copilot 最直接的貢獻就是大幅提升開發效率。想像一下,當你建立一個新的服務類別時,GitHub Copilot 可以:

  • 自動產生測試類別的基本架構
  • 根據方法簽名推導出常見的測試案例
  • 快速產生測試資料和 Mock 物件設定
  • 套用一致的測試命名規範

實際效果:原本需要 30 分鐘手動撰寫的測試程式碼,現在可能只需要 5-10 分鐘就能完成基礎架構,讓你有更多時間專注在測試邏輯的設計上。

品質保證:AI 協助識別遺漏的測試情境

GitHub Copilot 基於大量的開源程式碼訓練,具備識別常見測試模式的能力:

  • 邊界條件提醒:自動建議測試 null、空字串、極值等情況
  • 異常情況覆蓋:提醒測試各種例外狀況和錯誤處理
  • 測試完整性檢查:根據方法的複雜度建議相應的測試案例數量

知識傳承:將最佳實踐嵌入開發流程

每個團隊都有自己的測試規範和最佳實踐,GitHub Copilot 可以學習並複製這些模式:

  • 統一的測試命名規範
  • 一致的 3A 模式 (Arrange-Act-Assert) 結構
  • 標準化的 Mock 物件使用方式
  • 團隊慣用的斷言套件語法

學習加速:新手的最佳導師

對於測試新手來說,GitHub Copilot 就像一位經驗豐富的導師:

  • 提供即時的程式碼建議和範例
  • 展示正確的測試模式和結構
  • 協助理解複雜的測試概念
  • 減少查閱文件和範例的時間

GitHub Copilot 在測試場景的能力範圍

為了有效運用 GitHub Copilot,我們需要了解它的能力邊界:

適合的場景

單元測試案例產生
// GitHub Copilot 擅長根據方法簽名產生測試
public decimal CalculateDiscount(decimal price, decimal discountRate)
{
    // GitHub Copilot 會建議測試:正常情況、零值、負值、邊界值等
}
Mock 物件設定
// GitHub Copilot 能夠識別依賴並建議 Mock 設定
public class UserService
{
    private readonly IUserRepository _repository;
    private readonly IEmailService _emailService;
    
    // GitHub Copilot 會建議如何 Mock 這些依賴
}
測試資料準備
  • 自動產生測試所需的假資料
  • 建立複雜物件的建構器模式
  • 提供多樣化的測試案例資料
常見測試模式應用
  • 3A 模式的正確結構
  • 參數化測試的設定
  • 異常測試的寫法

限制與注意事項

GitHub Copilot 無法完全理解你的業務需求和領域邏輯,因此:

  • 無法判斷哪些測試案例對業務最關鍵
  • 可能忽略特定領域的邊界條件
  • 無法評估測試案例的業務價值

AI 產生的程式碼並非完美:

  • 測試邏輯可能不符合實際需求
  • 斷言可能過於簡單或不夠嚴謹
  • Mock 物件的行為設定可能不完整

雖然 GitHub Copilot 能產生很多測試,但測試品質和覆蓋率仍需人工把關:

  • 確保重要的程式碼路徑都有測試覆蓋
  • 驗證測試案例的有效性
  • 評估整體測試策略的完整性

人機協作的最佳實踐

要發揮 GitHub Copilot 的最大效用,關鍵在於明確分工:

AI 負責:基礎程式碼產生與模式套用

  • 程式碼結構產生:快速建立測試類別和方法結構
  • 常見模式套用:應用標準的測試模式和規範
  • 重複性工作處理:產生類似的測試案例和設定
  • 語法和格式規範:確保程式碼符合編碼標準

人類負責:邏輯驗證與品質把關

  • 業務邏輯驗證:確保測試符合實際業務需求
  • 測試策略設計:規劃整體的測試架構和優先級
  • 品質審查:檢視和改進 AI 產生的程式碼
  • 創新和優化:探索新的測試方法和工具

協作流程建議

  1. 明確測試目標:先定義清楚要測試什麼,再讓 GitHub Copilot 協助實作
  2. 段階性產生:分段讓 GitHub Copilot 產生程式碼,每段都進行審查
  3. 反覆調整:根據實際需求調整 GitHub Copilot 的建議
  4. 持續學習:觀察 GitHub Copilot 的建議模式,提升自己的測試技能

GitHub Copilot 客製化設定與配置

想要讓 GitHub Copilot 成為你的專屬測試助手,關鍵在於正確的客製化設定。透過建立專案特定的 Instructions 檔案和 Prompt 範本,我們可以讓 GitHub Copilot 理解並遵循團隊的測試規範。

建立三層級的客製化設定系統

為了達到最佳的測試開發體驗,我建議建立一個三層級的設定架構:

第一層:全域專案設定 (.github/copilot-instructions.md)

這是整個專案的基礎設定檔案,定義了專案層級的測試規範:

# 專案測試開發指導原則

## 技術棧資訊
- .NET 9
- 測試框架:xUnit v3 (3.0.1)
- Mock 框架:NSubstitute
- 斷言套件:AwesomeAssertions (不使用 FluentAssertions)

## 測試檔案組織結構
- 測試專案命名:`{ProjectName}.Tests`
- 測試類別命名:`{ClassName}Tests`
- 測試方法命名:`方法名_測試情境_預期結果`

## 測試程式碼規範
- 所有測試必須遵循 3A 模式 (Arrange-Act-Assert)
- 必須標註 `// Arrange`, `// Act`, `// Assert` 三個區塊
- 使用 AwesomeAssertions 的 `Should()` 語法進行斷言
- 變數名稱使用英文,類別、方法、屬性註解使用繁體中文

## 依賴注入與 Mock 規範
- 使用 NSubstitute 建立 Mock 物件
- 所有外部依賴都必須進行 Mock
- Mock 物件的命名格式:`mock{ServiceName}`

## 測試案例覆蓋要求
- 每個公開方法至少需要 3 個測試案例:正常情況、邊界條件、異常情況
- 使用 [Theory] 和 [InlineData] 進行參數化測試
- 異常處理必須使用 Assert.Throws 或 Should().Throw() 進行驗證

第二層:路徑特定設定 (.github/instructions/)

針對不同類型的測試建立專門的設定檔案:

單元測試規範

針對單元測試建立專門的設定檔案 unit-tests.instructions.md

---
applyTo: "tests/unit/**/*.cs"
---

# 單元測試專用指導

## 測試結構要求
- 每個測試類別對應一個被測試的類別
- 使用建構式進行測試初始化
- 複雜的測試資料準備使用 private 方法

## Mock 物件使用規範
- 每個測試方法都要重新設定 Mock 行為
- Mock 物件的驗證要明確且有意義
- 避免過度 Mock,只 Mock 必要的依賴

## 斷言規範
- 使用描述性的斷言訊息
- 複雜物件的比較要指定具體的屬性
- 集合的驗證要檢查元素內容,不只是數量

## 範例測試結構
```csharp
[Fact]
public void Add_輸入兩個正數_應回傳正確的和()
{
    // Arrange
    var calculator = new Calculator();
    var a = 5;
    var b = 3;
    var expected = 8;

    // Act
    var actual = calculator.Add(a, b);

    // Assert
    actual.Should().Be(expected);
}

整合測試規範

針對整合測試建立專門的設定檔案 integration-tests.instructions.md

---
applyTo: "tests/integration/**/*.cs"
---

# 整合測試專用指導

## 測試環境設定
- 使用 TestContainers 進行資料庫測試
- 每個測試類別都要實作 IClassFixture
- 測試資料要在每個測試後清理

## 資料庫測試規範
- 使用真實的資料庫連線進行測試
- 測試資料要有意義且符合實際業務情境
- 使用 Transaction 確保測試隔離性

## API 測試規範
- 使用 WebApplicationFactory 建立測試伺服器
- 驗證完整的 HTTP 回應內容
- 包含狀態碼、Header 和 Body 的檢查

第三層:目錄層級設定

在特定功能模組的測試目錄下建立 AGENTS.md 檔案:

# 使用者服務模組測試指導

## 業務邏輯重點
- 使用者註冊需要驗證 Email 格式
- 密碼強度檢查包含長度和複雜度要求
- 重複註冊要回傳明確的錯誤訊息

## 測試重點關注
- Email 驗證的各種邊界情況
- 密碼安全性規則的完整測試
- 資料庫異常情況的處理

## 常用測試資料
- 有效 Email:`test@example.com`
- 無效 Email:`invalid-email`, `@example.com`, `test@`
- 有效密碼:`SecurePass123!`
- 無效密碼:`123`, `password`, `ABCDEFGH`

測試專用 Prompt 範本設計

除了 Instructions 檔案,我們還可以建立一套標準化的 Prompt 範本,讓團隊成員能夠快速產生高品質的測試程式碼。

xUnit 完整測試類別產生範本

為以下 C# 類別建立完整的 xUnit 測試類別,要求如下:

1. **測試框架規範**:
   - 使用 xUnit v3 (3.0.1)
   - 使用 NSubstitute 進行 Mock
   - 使用 AwesomeAssertions 進行斷言

2. **測試結構要求**:
   - 遵循 3A 模式並標註註解 (Arrange, Act, Assert)
   - 測試命名:`方法名_測試情境_預期結果`
   - 至少包含正常情況、邊界條件、異常情況測試

3. **程式碼品質**:
   - 所有外部依賴都要 Mock
   - 使用參數化測試處理多個類似案例
   - 包含有意義的斷言訊息

4. **特殊要求**:
   - 測試類別命名:`{原類別名}Tests`
   - 每個測試方法都要有完整的 XML 註解
   - 複雜的測試資料使用 private 方法建立

請為以下類別產生測試:

[貼上要測試的類別程式碼]

異常處理測試專用範本

為指定的方法建立異常處理測試,包含:

1. **異常類型測試**:
   - 驗證拋出正確的異常類型
   - 檢查異常訊息的內容
   - 確認異常的 InnerException(如果有的話)

2. **異常觸發條件**:
   - 識別所有可能導致異常的輸入
   - 測試各種無效參數組合
   - 模擬依賴服務的異常情況

3. **斷言方式**:
   ```csharp
   // 使用 AwesomeAssertions 語法
   action.Should().Throw<SpecificException>()
       .WithMessage("Expected error message");

目標方法:[貼上方法簽名]


#### Mock 物件設定範本

```text
為以下服務類別建立完整的 Mock 設定,要求:

1. **Mock 物件建立**:
   - 使用 `NSubstitute.Substitute.For<T>()`
   - 命名格式:mock{ServiceName}
   - 在測試類別的建構式中初始化

2. **行為設定**:
   - 為每個依賴方法設定回傳值
   - 包含正常和異常情況的模擬
   - 使用 Returns() 和 Throws() 方法

3. **驗證設定**:
   - 使用 Received() 驗證方法呼叫
   - 檢查傳入參數的正確性
   - 驗證呼叫次數

類別依賴:[列出所有依賴介面]

VS Code 環境設定優化

要充分發揮 GitHub Copilot 的能力,正確的 VS Code 設定是必不可少的。

基本 GitHub Copilot 設定

在 VS Code 的 settings.json 中加入以下設定:

{
  // 啟用測試產生的 CodeLens 功能
  "github.copilot.chat.generateTests.codeLens": true,
  
  // 設定 GitHub Copilot 的啟用範圍
  "github.copilot.enable": {
    "*": true,
    "yaml": false,
    "plaintext": false
  },
  
  // 設定 GitHub Copilot 建議的觸發方式
  "github.copilot.inlineSuggest.enable": true,
  
  // 啟用 Agent Mode
  "github.copilot.chat.agent.enabled": true
}

測試開發專用快捷鍵

設定自訂的快捷鍵來快速存取常用的 GitHub Copilot 功能:

{
  // keybindings.json
  [
    {
      "key": "ctrl+shift+t",
      "command": "workbench.action.chat.open",
      "when": "editorTextFocus"
    },
    {
      "key": "ctrl+shift+i",
      "command": "workbench.action.chat.openInlineChat",
      "when": "editorTextFocus"
    },
    {
      "key": "ctrl+shift+e",
      "command": "workbench.action.chat.openQuickChat",
      "when": "editorTextFocus"
    }
  ]
}

Workspace 特定設定

在專案根目錄的 .vscode/settings.json 中設定專案特定的 GitHub Copilot 行為:

{
  // 專案特定的 GitHub Copilot 設定
  "github.copilot.chat.customInstructions": [
    "Always use xUnit v3 for testing framework",
    "Use NSubstitute for mocking",
    "Use AwesomeAssertions for assertions",
    "Follow AAA pattern with comments",
    "Use Traditional Chinese for comments and documentation"
  ],
  
  // 測試檔案的特殊設定
  "files.associations": {
    "*Tests.cs": "csharp-test"
  },
  
  // 自動格式化設定
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.organizeImports": true
  }
}

xUnit v3 專案設定重點

由於範例專案使用 xUnit v3,有幾個重要的設定需要特別注意:

測試專案檔案設定

xUnit v3 需要在測試專案檔案中加入特定設定:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <OutputType>Exe</OutputType>
    <EnableMicrosoftTestingPlatform>true</EnableMicrosoftTestingPlatform>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="xunit.v3" Version="3.0.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
    <!-- 其他套件... -->
  </ItemGroup>
</Project>

重要設定說明:

  • OutputType 必須設為 Exe(xUnit v3 的測試專案是可執行檔)
  • EnableMicrosoftTestingPlatform 啟用新的測試平台
  • 使用 xunit.v3 套件而非 xunit

xUnit v3 執行設定

建立 xunit.runner.json 來設定測試執行行為:

{
    "parallelAlgorithm": "conservative",
    "maxParallelThreads": 4,
    "diagnosticMessages": true,
    "failSkips": false,
    "preEnumerateTheories": false
}

測試案例的 AI 產生技巧

有了正確的設定基礎,現在我們來學習如何撰寫有效的提示 (Prompt),讓 GitHub Copilot 產生符合需求的測試程式碼。

結構化提示的撰寫原則

標準提示架構

一個有效的測試產生提示應該包含以下四個核心要素:

1. 明確指定測試框架和工具
2. 描述測試範圍和重點  
3. 指定預期的測試模式
4. 要求特定的斷言風格

實戰範例:完整的 Prompt 模板

請為以下 C# 類別建立 xUnit 測試類別,具體要求:

【框架規範】
- 測試框架:xUnit v3 (3.0.1)
- Mock 框架:NSubstitute  
- 斷言套件:AwesomeAssertions (使用 Should() 語法)

【測試範圍】
- 測試所有公開方法
- 涵蓋正常情況、邊界條件、異常情況
- 特別注意 null 值和空字串的處理

【結構要求】
- 3A 模式並標註 // Arrange, // Act, // Assert
- 命名規範:方法名_測試情境_預期結果
- 使用 [Theory] 和 [InlineData] 處理多個測試案例

【品質標準】
- 每個外部依賴都要建立 Mock
- 斷言要包含描述性訊息
- 測試方法要有完整的 XML 註解

[在此貼上要測試的類別程式碼]

十個實用 Prompt 範本集

以下提供十個經過實戰驗證的 Prompt 範本,幫助你快速產生高品質的測試程式碼。

如何在 VS Code 中使用這些 Prompt 範本

方法一:使用 GitHub Copilot Chat
  1. 開啟 Chat 面板:按下 Ctrl+Shift+I (Windows) 或 Cmd+Shift+I (Mac)
  2. 選擇程式碼:在編輯器中選取要測試的類別或方法
  3. 輸入範本:複製以下任一範本到 Chat 中
  4. 替換參數:將範本中的 [參數] 替換為實際的程式碼內容
  5. 執行產生:按下 Enter,GitHub Copilot 將產生對應的測試程式碼
方法二:使用 Inline Chat
  1. 定位程式碼:將游標放在要測試的方法上
  2. 開啟 Inline Chat:按下 Ctrl+I (Windows) 或 Cmd+I (Mac)
  3. 輸入指令:使用 /tests 命令加上自訂範本內容
  4. 即時產生:測試程式碼會直接在適當位置產生

範例使用流程:

1. 選取 CalculateDiscount 方法
2. 開啟 Chat 並輸入:「使用範本 1 產生基礎單元測試」
3. 貼上範本內容並將 [方法簽名] 替換為實際方法
4. GitHub Copilot 產生完整的測試類別

1. 基礎單元測試產生

產生基礎單元測試,包含:
- 正常輸入的測試案例
- 使用真實的測試資料
- 簡潔明確的斷言
- 標準的 3A 結構

目標方法:[方法簽名]

2. 邊界條件測試專用

專注於邊界條件測試,涵蓋:
- null、空字串、空集合
- 數值的最大值、最小值、零值
- 字串長度的邊界情況
- 日期時間的特殊值

使用參數化測試來組織這些案例。

3. 異常處理測試

建立異常處理測試,確保:
- 使用 Should().Throw<ExceptionType>() 語法
- 驗證異常訊息的內容
- 測試所有可能觸發異常的條件
- 檢查異常後系統狀態的正確性

4. 非同步方法測試

為非同步方法建立測試,注意:
- 使用 async/await 語法
- 測試 Task 和 Task<T> 的回傳
- 處理 CancellationToken 的情況
- 驗證非同步操作的完成狀態

方法簽名:[async 方法]

5. 相依性注入測試

建立包含依賴注入的測試:
- 使用 NSubstitute 建立所有依賴的 Mock
- 設定 Mock 物件的預期行為
- 驗證依賴方法的呼叫次數和參數
- 測試依賴服務異常時的處理

類別依賴:[列出依賴清單]

6. 資料驗證測試

專門測試資料驗證邏輯:
- 有效資料的通過情況
- 各種無效資料的拒絕情況
- 驗證錯誤訊息的準確性
- 複合驗證規則的測試

驗證規則:[描述驗證邏輯]

7. 集合操作測試

測試集合相關的操作:
- 空集合的處理
- 單一元素和多元素的情況
- 集合轉換和過濾的正確性
- 使用 Should().BeEquivalentTo() 比較集合內容

操作類型:[描述集合操作]

8. 狀態變更測試

測試物件狀態變更:
- 檢查初始狀態
- 驗證操作後的狀態變化
- 確保狀態變更的原子性
- 測試狀態變更的副作用

狀態變更邏輯:[描述狀態變更]

9. 效能相關測試

建立效能相關的測試:
- 測試大數據量的處理能力
- 驗證超時機制的運作
- 檢查記憶體使用是否合理
- 使用 StopWatch 測量執行時間

效能要求:[描述效能標準]

10. 整合測試產生

建立整合測試案例:
- 使用真實的外部依賴
- 設定完整的測試環境
- 測試端到端的業務流程
- 包含清理和重置邏輯

整合範圍:[描述整合邊界]

測試情境描述策略

正常流程測試

描述策略要點:

  • 具體化輸入:提供真實且有意義的測試資料
  • 完整性檢查:確保覆蓋主要的業務流程
  • 結果驗證:不只檢查回傳值,也要驗證副作用

範例描述:

測試使用者註冊的正常流程:
- 輸入:有效的 Email (user@example.com) 和強密碼 (SecurePass123!)
- 預期:成功建立使用者帳戶,回傳使用者ID,發送歡迎郵件
- 驗證:資料庫記錄、郵件服務呼叫、回傳值格式

邊界條件測試

重點關注的邊界:

數值邊界測試

測試計算折扣的邊界條件:

  • 折扣率 0%:應該回傳原價
  • 折扣率 100%:應該回傳 0
  • 折扣率 -1%:應該拋出 ArgumentException
  • 折扣率 101%:應該拋出 ArgumentException
字串邊界測試

測試使用者名稱驗證:

  • 空字串:應該拒絕
  • 單一字元:應該拒絕
  • 最小長度 (3 字元):應該接受
  • 最大長度 (50 字元):應該接受
  • 超過最大長度:應該拒絕
集合邊界測試

測試批次處理功能:

  • 空集合:應該回傳空結果
  • 單一元素:應該正常處理
  • 大量元素 (1000+):應該分批處理
  • null 集合:應該拋出 ArgumentNullException

異常情況測試

系統性的異常測試策略:

輸入驗證異常
測試所有無效輸入組合:
- null 參數:ArgumentNullException
- 空字串參數:ArgumentException  
- 格式錯誤:FormatException
- 範圍錯誤:ArgumentOutOfRangeException
外部服務異常
模擬外部依賴的各種失敗情況:
- 網路連線失敗:HttpRequestException
- 服務暫時不可用:ServiceUnavailableException
- 認證失敗:UnauthorizedException
- 資料不一致:InvalidOperationException

使用 GitHub Copilot Chat 進行測試初始化

GitHub Copilot 提供了多種方式來快速初始化測試環境,包括 /tests 指令和自然語言提示。

使用 /tests 指令產生測試

最直接的方式是使用 /tests 指令來為選取的程式碼產生測試:

  1. 在 VS Code 中選取要測試的類別或方法
  2. 開啟 GitHub Copilot Chat
  3. 輸入 /tests 指令

自然語言提示初始化測試專案

對於更複雜的測試環境設定,使用詳細的自然語言提示效果更好:

初始化新的測試專案
請幫我建立一個 xUnit v3 測試專案,包含:
- NSubstitute Mock 框架
- AwesomeAssertions 斷言套件
- 基本的測試類別結構
- GlobalUsings.cs 檔案
- 適當的 .csproj 設定
為現有類別建立完整測試套件
為 UserService 類別建立完整的測試套件:
- 覆蓋所有公開方法
- 包含正常情況、邊界條件、異常情況測試
- 使用 NSubstitute 建立 Mock 物件
- 目標覆蓋率 90% 以上
- 遵循 3A 模式並加上註解
設定整合測試環境
建立整合測試環境設定:
- 使用 WebApplicationFactory 建立測試伺服器
- 設定 TestContainers 用於資料庫測試
- 包含測試資料的初始化和清理
- 建立基礎測試類別供其他測試繼承

使用 Agent Mode 進行全專案設定

啟用 GitHub Copilot 的 Agent Mode 可以讓 AI 主動執行多個步驟:

在 Agent Mode 中輸入:
請設定一個完整的 .NET 9 測試專案,包含 xUnit v3、NSubstitute 和 AwesomeAssertions,並建立適當的專案結構。

Agent Mode 會自動:

  • 建立必要的檔案和目錄
  • 安裝需要的 NuGet 套件
  • 設定專案檔案
  • 建立範例測試類別

與現有專案的整合

專案套件安裝
# 確保專案已安裝必要套件
dotnet add package xunit.v3 --version 3.0.1
dotnet add package xunit.runner.visualstudio --version 3.1.4
dotnet add package NSubstitute --version 5.3.0
dotnet add package AwesomeAssertions --version 9.1.0
建立測試基礎結構

使用 GitHub Copilot 產生測試基礎類別:

// 由 GitHub Copilot 產生的整合測試基礎類別
public class IntegrationTestBase : IClassFixture<WebApplicationFactory<Program>>
{
    protected readonly WebApplicationFactory<Program> Factory;
    protected readonly HttpClient Client;
    protected readonly IServiceScope Scope;

    public IntegrationTestBase(WebApplicationFactory<Program> factory)
    {
        Factory = factory;
        Client = factory.CreateClient();
        Scope = factory.Services.CreateScope();
    }

    protected T GetService<T>() where T : notnull
    {
        return Scope.ServiceProvider.GetRequiredService<T>();
    }

    public virtual void Dispose()
    {
        Client?.Dispose();
        Scope?.Dispose();
    }
}

進階 AI 輔助技術

當你熟悉了基本的測試產生技巧後,是時候探索 GitHub Copilot 的進階功能,讓 AI 成為你測試開發的智慧夥伴。

測試重構的 AI 協助

識別並提取重複的測試程式碼

GitHub Copilot 能夠識別測試程式碼中的重複模式,並建議重構方案:

重構前的重複程式碼
[Fact]
public void CreateUser_ValidInput_ShouldReturnUser()
{
    // Arrange
    var mockRepository = Substitute.For<IUserRepository>();
    var mockEmailService = Substitute.For<IEmailService>();
    var userService = new UserService(mockRepository, mockEmailService);
    
    // Act & Assert
    // ...
}

[Fact]
public void CreateUser_InvalidEmail_ShouldThrowException()
{
    // Arrange
    var mockRepository = Substitute.For<IUserRepository>();
    var mockEmailService = Substitute.For<IEmailService>();
    var userService = new UserService(mockRepository, mockEmailService);
    
    // Act & Assert
    // ...
}
GitHub Copilot 建議的重構結果
public class UserServiceTests
{
    private readonly IUserRepository _mockRepository;
    private readonly IEmailService _mockEmailService;
    private readonly UserService _userService;

    public UserServiceTests()
    {
        _mockRepository = Substitute.For<IUserRepository>();
        _mockEmailService = Substitute.For<IEmailService>();
        _userService = new UserService(_mockRepository, _mockEmailService);
    }

    [Fact]
    public void CreateUser_ValidInput_ShouldReturnUser()
    {
        // Arrange
        var validUser = CreateValidTestUser();
        
        // Act & Assert
        // ...
    }

    private User CreateValidTestUser()
    {
        return new User
        {
            Email = "test@example.com",
            Name = "Test User",
            Password = "SecurePass123!"
        };
    }
}

改善測試可讀性的 AI 建議

問題識別 Prompt
分析以下測試程式碼的可讀性問題,並提供改善建議:

重點檢查:
1. 測試意圖是否清楚
2. 測試資料是否有意義
3. 斷言是否具體明確
4. 註解是否必要且有幫助

[貼上需要檢查的測試程式碼]
GitHub Copilot 回應範例
// 改善前:意圖不明確
[Fact]
public void Test1()
{
    var result = _service.Process("abc", 123);
    result.Should().NotBeNull();
}

// 改善後:意圖清楚
[Fact]
public void Process_ValidUsernameAndAge_ShouldReturnUserProfile()
{
    // Arrange
    var username = "john_doe";
    var age = 25;
    var expectedProfile = new UserProfile { Name = username, Age = age };

    // Act
    var actualProfile = _service.Process(username, age);

    // Assert
    actualProfile.Should().NotBeNull();
    actualProfile.Name.Should().Be(username);
    actualProfile.Age.Should().Be(age);
}

Mock 物件的智慧產生

自動識別需要 Mock 的依賴

GitHub Copilot 能夠分析類別的依賴關係,並自動建議完整的 Mock 設定:

智慧 Mock 產生 Prompt
分析以下類別的依賴關係,並產生完整的 Mock 設定:

要求:
1. 識別所有需要 Mock 的依賴介面
2. 為每個依賴建立適當的 Mock 物件
3. 設定常見情境的回傳值和行為
4. 包含異常情況的模擬

[貼上類別程式碼]
產生的完整 Mock 設定
public class OrderServiceTests
{
    private readonly IOrderRepository _mockOrderRepository;
    private readonly IPaymentService _mockPaymentService;
    private readonly IInventoryService _mockInventoryService;
    private readonly INotificationService _mockNotificationService;
    private readonly OrderService _orderService;

    public OrderServiceTests()
    {
        // 建立 Mock 物件
        _mockOrderRepository = Substitute.For<IOrderRepository>();
        _mockPaymentService = Substitute.For<IPaymentService>();
        _mockInventoryService = Substitute.For<IInventoryService>();
        _mockNotificationService = Substitute.For<INotificationService>();
        
        // 初始化待測試服務
        _orderService = new OrderService(
            _mockOrderRepository,
            _mockPaymentService,
            _mockInventoryService,
            _mockNotificationService
        );
        
        // 設定預設的 Mock 行為
        SetupDefaultMockBehaviors();
    }

    private void SetupDefaultMockBehaviors()
    {
        // 庫存服務預設行為
        _mockInventoryService
            .CheckAvailability(Arg.Any<int>(), Arg.Any<int>())
            .Returns(true);
        
        // 付款服務預設行為
        _mockPaymentService
            .ProcessPayment(Arg.Any<decimal>(), Arg.Any<string>())
            .Returns(new PaymentResult { Success = true, TransactionId = "TX123" });
        
        // 訂單倉庫預設行為
        _mockOrderRepository
            .Save(Arg.Any<Order>())
            .Returns(callInfo => callInfo.Arg<Order>());
    }

    // 設定特定測試情境的 Mock 行為
    private void SetupInventoryShortage()
    {
        _mockInventoryService
            .CheckAvailability(Arg.Any<int>(), Arg.Any<int>())
            .Returns(false);
    }

    private void SetupPaymentFailure()
    {
        _mockPaymentService
            .ProcessPayment(Arg.Any<decimal>(), Arg.Any<string>())
            .Returns(new PaymentResult { Success = false, ErrorMessage = "信用卡餘額不足" });
    }
}

NSubstitute 最佳實踐的自動應用

驗證互動的智慧產生
[Fact]
public void CreateOrder_SuccessfulFlow_ShouldCallAllDependencies()
{
    // Arrange
    var order = CreateValidTestOrder();
    
    // Act
    var result = _orderService.CreateOrder(order);
    
    // Assert - 驗證所有依賴的正確互動
    _mockInventoryService
        .Received(1)
        .CheckAvailability(order.ProductId, order.Quantity);
    
    _mockPaymentService
        .Received(1)
        .ProcessPayment(order.TotalAmount, order.PaymentMethod);
    
    _mockOrderRepository
        .Received(1)
        .Save(Arg.Is<Order>(o => o.Status == OrderStatus.Confirmed));
    
    _mockNotificationService
        .Received(1)
        .SendOrderConfirmation(order.CustomerId, Arg.Any<string>());
}

參數化測試的自動產生

從業務邏輯推導測試案例

GitHub Copilot 能夠分析方法的業務邏輯,自動產生涵蓋各種情境的參數化測試:

智慧參數化測試產生
public class DiscountCalculatorTests
{
    private readonly DiscountCalculator _calculator = new();

    [Theory]
    [InlineData(100, 0, 100, "無折扣情況")]
    [InlineData(100, 0.1, 90, "10% 折扣")]
    [InlineData(100, 0.5, 50, "50% 折扣")]
    [InlineData(100, 1.0, 0, "100% 折扣(免費)")]
    [InlineData(0, 0.1, 0, "原價為零的情況")]
    [InlineData(99.99, 0.15, 84.99, "小數點計算精確度")]
    public void CalculateDiscountedPrice_各種折扣情況_應回傳正確價格(
        decimal originalPrice, 
        decimal discountRate, 
        decimal expectedPrice,
        string scenario)
    {
        // Act
        var actualPrice = _calculator.CalculateDiscountedPrice(originalPrice, discountRate);

        // Assert
        actualPrice.Should().Be(expectedPrice, $"測試情境:{scenario}");
    }

    [Theory]
    [InlineData(-1, 0.1, "負數原價")]
    [InlineData(100, -0.1, "負數折扣率")]
    [InlineData(100, 1.1, "折扣率超過 100%")]
    public void CalculateDiscountedPrice_無效輸入_應拋出ArgumentException(
        decimal originalPrice,
        decimal discountRate,
        string scenario)
    {
        // Act & Assert
        var action = () => _calculator.CalculateDiscountedPrice(originalPrice, discountRate);
        
        action.Should().Throw<ArgumentException>($"測試情境:{scenario}");
    }
}

複雜資料結構的測試案例設計

使用 MemberData 的進階參數化測試
public class UserValidationTests
{
    private readonly UserValidator _validator = new();

    [Theory]
    [MemberData(nameof(GetValidUserTestCases))]
    public void ValidateUser_有效用戶資料_應通過驗證(User user, string scenario)
    {
        // Act
        var result = _validator.ValidateUser(user);

        // Assert
        result.IsValid.Should().BeTrue($"測試情境:{scenario}");
        result.Errors.Should().BeEmpty();
    }

    [Theory]
    [MemberData(nameof(GetInvalidUserTestCases))]
    public void ValidateUser_無效用戶資料_應失敗並返回錯誤(User user, string expectedError, string scenario)
    {
        // Act
        var result = _validator.ValidateUser(user);

        // Assert
        result.IsValid.Should().BeFalse($"測試情境:{scenario}");
        result.Errors.Should().Contain(expectedError);
    }

    // GitHub Copilot 自動產生的測試資料
    public static IEnumerable<object[]> GetValidUserTestCases()
    {
        yield return new object[] 
        { 
            new User { Email = "test@example.com", Age = 18, Name = "Valid User" },
            "最小年齡用戶" 
        };
        
        yield return new object[] 
        { 
            new User { Email = "senior@example.com", Age = 99, Name = "Senior User" },
            "高齡用戶" 
        };
        
        yield return new object[] 
        { 
            new User { Email = "unicode@測試.com", Age = 30, Name = "Unicode測試" },
            "包含 Unicode 字符的用戶" 
        };
    }

    public static IEnumerable<object[]> GetInvalidUserTestCases()
    {
        yield return new object[] 
        { 
            new User { Email = "invalid-email", Age = 25, Name = "Test" },
            "Email 格式無效",
            "無效的 Email 格式" 
        };
        
        yield return new object[] 
        { 
            new User { Email = "test@example.com", Age = 17, Name = "Minor" },
            "年齡必須大於等於 18",
            "未成年用戶" 
        };
        
        yield return new object[] 
        { 
            new User { Email = "test@example.com", Age = 25, Name = "" },
            "姓名不能為空",
            "空姓名" 
        };
    }
}

組合測試的自動產生

對於需要測試多個參數組合的情況,GitHub Copilot 可以產生全面的組合測試:

[Theory]
[MemberData(nameof(GetUserRolePermissionCombinations))]
public void CheckPermission_不同角色和權限組合_應回傳正確結果(
    UserRole role,
    Permission permission,
    bool expectedResult,
    string scenario)
{
    // Arrange
    var user = new User { Role = role };
    
    // Act
    var hasPermission = _authorizationService.HasPermission(user, permission);
    
    // Assert
    hasPermission.Should().Be(expectedResult, $"情境:{scenario}");
}

public static IEnumerable<object[]> GetUserRolePermissionCombinations()
{
    // 管理員權限測試
    foreach (var permission in Enum.GetValues<Permission>())
    {
        yield return new object[] 
        { 
            UserRole.Admin, 
            permission, 
            true, 
            $"管理員應該擁有 {permission} 權限" 
        };
    }
    
    // 一般用戶權限測試
    var userPermissions = new[] { Permission.Read, Permission.Update };
    foreach (var permission in Enum.GetValues<Permission>())
    {
        var hasPermission = userPermissions.Contains(permission);
        yield return new object[] 
        { 
            UserRole.User, 
            permission, 
            hasPermission, 
            $"一般用戶對 {permission} 權限的檢查" 
        };
    }
    
    // 訪客權限測試
    foreach (var permission in Enum.GetValues<Permission>())
    {
        var hasPermission = permission == Permission.Read;
        yield return new object[] 
        { 
            UserRole.Guest, 
            permission, 
            hasPermission, 
            $"訪客對 {permission} 權限的檢查" 
        };
    }
}

專案層級的測試自動化設定

當個人開發技巧成熟後,下一步是將 AI 輔助測試技術推廣到整個團隊和專案層級。這個階段的重點是建立開發階段的自動化工具和規範,讓團隊成員在本機開發時就能確保測試品質的一致性。

建立專案專用的測試規範文件

測試策略文件範本

建立 docs/testing-strategy.md 來定義專案的測試方針:

# 專案測試策略

## 測試金字塔配置
- **單元測試 (70%)**:快速、獨立、覆蓋業務邏輯
- **整合測試 (20%)**:驗證組件間的互動
- **端到端測試 (10%)**:關鍵業務流程的完整驗證

## AI 輔助開發規範
- 所有新功能都要使用 GitHub Copilot 產生初始測試結構
- 測試產生後必須進行人工審查和調整
- 使用標準化的 Prompt 範本確保一致性

## 品質標準與開發階段檢查
- 程式碼覆蓋率目標:85% 以上
- 每個功能都要包含對應的測試案例
- 開發者在本機執行測試時間:單元測試 < 5 分鐘,整合測試 < 15 分鐘

程式碼覆蓋率目標設定

coverlet.runsettings 中設定覆蓋率標準,讓開發者在本機就能檢查覆蓋率:

<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="XPlat code coverage">
        <Configuration>
          <Format>cobertura</Format>
          <Threshold>85</Threshold>
          <ThresholdType>line</ThresholdType>
          <ThresholdStat>total</ThresholdStat>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
</RunSettings>

開發者可以在本機執行以下指令來檢查覆蓋率:

# 執行測試並產生覆蓋率報告
dotnet test --settings coverlet.runsettings --collect:"XPlat Code Coverage"

# 檢查是否達到設定的覆蓋率門檻
dotnet test --settings coverlet.runsettings --collect:"XPlat Code Coverage" --logger:console

開發階段的程式碼品質檢查自動化

為了確保團隊成員在開發階段就能維持測試程式碼的品質,我們可以建立一套本機可執行的檢查工具。

整合 EditorConfig 進行程式碼格式統一

.editorconfig 設定範例:

[*.cs]
# 測試檔案特殊規則
[*Tests.cs]
dotnet_naming_rule.test_methods_should_be_descriptive.severity = error
dotnet_naming_rule.test_methods_should_be_descriptive.symbols = test_methods
dotnet_naming_rule.test_methods_should_be_descriptive.style = test_method_style

dotnet_naming_symbols.test_methods.applicable_kinds = method
dotnet_naming_symbols.test_methods.applicable_accessibilities = public
dotnet_naming_symbols.test_methods.required_modifiers = 

dotnet_naming_style.test_method_style.required_prefix = 
dotnet_naming_style.test_method_style.required_suffix = 
dotnet_naming_style.test_method_style.word_separator = _
dotnet_naming_style.test_method_style.capitalization = pascal_case

開發者本機測試命名檢查

建立 PowerShell 腳本 scripts/validate-test-names.ps1,讓開發者在提交程式碼前能在本機檢查測試命名規範:

# Test naming convention validator
param(
    [string]$TestProjectPath = "tests"
)

Write-Host "Checking test naming conventions..." -ForegroundColor Green

$pattern = "^[A-Z][a-zA-Z0-9]*_[A-Za-z0-9\u4e00-\u9fff]+_[A-Za-z0-9\u4e00-\u9fff]+$"
$invalidTests = @()

Get-ChildItem -Path $TestProjectPath -Recurse -Filter "*.cs" | ForEach-Object {
    $content = Get-Content $_.FullName -Encoding UTF8

    $content | Select-String -Pattern "\[Fact\]|\[Theory\]" -Context 0, 1 | ForEach-Object {
        $methodLine = $_.Context.PostContext[0]
        if ($methodLine -match "public void (\w+)\(") {
            $methodName = $matches[1]
            if ($methodName -notmatch $pattern) {
                $invalidTests += @{
                    File   = $_.Filename
                    Method = $methodName
                    Line   = $_.LineNumber + 1
                }
            }
        }
    }
}

if ($invalidTests.Count -gt 0) {
    Write-Host "Found test methods with invalid naming:" -ForegroundColor Red
    $invalidTests | ForEach-Object {
        Write-Host "  $($_.File):$($_.Line) - $($_.Method)" -ForegroundColor Yellow
    }
    Write-Host "`nCorrect naming format: MethodName_TestScenario_ExpectedResult" -ForegroundColor Cyan
    Write-Host "Example: RegisterUser_ValidData_ShouldCreateUser" -ForegroundColor Cyan
    exit 1
}
else {
    Write-Host "All test method names follow the naming convention!" -ForegroundColor Green
}

使用方式

# 檢查整個測試目錄
pwsh scripts/validate-test-names.ps1

# 檢查特定測試專案
pwsh scripts/validate-test-names.ps1 -TestProjectPath "tests/unit"

這個腳本幫助開發者在本機就能確保測試命名符合團隊規範,避免在程式碼審查階段才發現問題。


實戰演練與總結

完整實作演示

讓我們通過一個完整的範例來展示如何運用本文所學的技術。

情境:為使用者服務建立完整的測試套件

步驟 1:建立專案結構
# 建立專案目錄
mkdir UserManagementSystem
cd UserManagementSystem

# 建立解決方案和專案結構
dotnet new sln -n UserManagement
dotnet new classlib -n UserManagement.Core -o src/UserManagement.Core
dotnet new xunit -n UserManagement.Tests -o tests/UserManagement.Tests
dotnet sln add src/UserManagement.Core tests/UserManagement.Tests
步驟 2:設定客製化 Instructions

建立 .github/copilot-instructions.md

# 使用者管理系統測試指導

## 技術棧
- .NET 9, xUnit v3, NSubstitute, AwesomeAssertions

## 業務規則
- 使用者註冊需要驗證 Email 唯一性
- 密碼必須符合複雜度要求(8-50字元,包含數字和字母)
- 使用者狀態:Active, Inactive, Suspended

## 測試重點
- Email 格式和唯一性驗證
- 密碼安全性檢查
- 使用者狀態轉換邏輯
- 異常處理的完整性
步驟 3:使用 GitHub Copilot 產生測試

UserService 類別使用我們的 Prompt 範本:

為 UserService 類別建立完整的 xUnit 測試,特別關注:

【業務邏輯】
- 使用者註冊的 Email 唯一性檢查
- 密碼複雜度驗證(8-50字元,包含數字和字母)
- 使用者狀態管理(Active, Inactive, Suspended)

【測試覆蓋】
- 正常註冊流程
- 重複 Email 註冊
- 各種無效密碼格式
- 狀態轉換的所有可能組合

【技術要求】
- 使用 NSubstitute Mock IUserRepository 和 IEmailService
- 使用 AwesomeAssertions 進行斷言
- 包含參數化測試處理多種情境
產生的測試類別(部分)
public class UserServiceTests
{
    private readonly IUserRepository _mockUserRepository;
    private readonly IEmailService _mockEmailService;
    private readonly UserService _userService;

    public UserServiceTests()
    {
        _mockUserRepository = Substitute.For<IUserRepository>();
        _mockEmailService = Substitute.For<IEmailService>();
        _userService = new UserService(_mockUserRepository, _mockEmailService);
    }

    [Fact]
    public void RegisterUser_有效資料_應成功建立使用者()
    {
        // Arrange
        var registerRequest = new RegisterUserRequest
        {
            Email = "newuser@example.com",
            Password = "SecurePass123",
            Name = "新使用者"
        };

        _mockUserRepository
            .ExistsByEmail(registerRequest.Email)
            .Returns(false);

        // Act
        var result = _userService.RegisterUser(registerRequest);

        // Assert
        result.Should().NotBeNull();
        result.Email.Should().Be(registerRequest.Email);
        result.Status.Should().Be(UserStatus.Active);
        
        _mockUserRepository.Received(1).Save(Arg.Any<User>());
        _mockEmailService.Received(1).SendWelcomeEmail(registerRequest.Email);
    }

    [Theory]
    [InlineData("", "密碼不能為空")]
    [InlineData("123", "密碼長度至少 8 字元")]
    [InlineData("12345678", "密碼必須包含字母")]
    [InlineData("abcdefgh", "密碼必須包含數字")]
    [InlineData("a".PadRight(51, '1'), "密碼長度不能超過 50 字元")]
    public void RegisterUser_無效密碼_應拋出ValidationException(string password, string expectedMessage)
    {
        // Arrange
        var registerRequest = new RegisterUserRequest
        {
            Email = "test@example.com",
            Password = password,
            Name = "測試使用者"
        };

        // Act & Assert
        var action = () => _userService.RegisterUser(registerRequest);
        
        action.Should().Throw<ValidationException>()
            .WithMessage(expectedMessage);
    }
}

效果評估與限制分析

開發效率提升的量化指標

基於團隊實際使用經驗和觀察,引入 GitHub Copilot 後的效果:

時間節省
  • 測試程式碼撰寫時間:明顯減少
  • Mock 物件設定時間:大幅縮短
  • 測試案例設計時間:顯著改善
品質提升
  • 測試覆蓋率:整體提升
  • 邊界條件覆蓋:更加完整
  • 測試命名一致性:明顯改善
學習效果
  • 新手上手時間:大幅縮短
  • 測試模式掌握:更加快速
  • 最佳實踐應用:更易採用

AI 輔助的局限性認知

技術限制
  • 無法理解複雜的業務邏輯關聯
  • 對領域特定知識的理解有限
  • 產生的測試可能缺乏創意性思考
需要人工把關的環節
  • 測試案例的業務價值評估
  • 複雜整合場景的設計
  • 效能測試的策略規劃
  • 安全性測試的考量
持續改進建議
  • 定期更新 Instructions 檔案
  • 收集團隊使用回饋並優化 Prompt
  • 建立測試案例品質的審查機制
  • 培養團隊的 AI 協作技能

未來發展趨勢預測

技術演進方向
  • 更智慧的業務邏輯理解能力
  • 自動化的測試覆蓋率分析
  • 與 CI/CD 更深度的整合
  • 跨團隊的知識共享機制
組織層面的變化
  • 測試開發角色的轉變
  • 新的程式碼審查流程
  • AI 輔助開發的標準化
  • 測試策略的重新思考

結語

GitHub Copilot 正在改變我們開發測試的方式。透過適當的設定和技巧,它不僅能大幅提升開發效率,更能幫助我們建立更完整、更一致的測試套件。

但請記住,AI 是工具,不是替代品。最好的測試仍然需要人類的創意、經驗和對業務的深度理解。讓我們善用 AI 的優勢,專注在更有價值的測試策略設計和品質把關上。

未來的測試開發將是人機協作的新模式。掌握這些技能,你就能在這個變革中保持領先地位。

參考資源

GitHub Copilot 官方文件

Microsoft Learn 學習資源

社群資源與設定

範例程式碼


這是「重啟挑戰:老派軟體工程師的測試修練」的第二十七天。明天會介紹 Day 28 – TUnit 入門:下世代 .NET 測試框架探索。


上一篇
Day 26 – xUnit 升級指南:從 2.9.x 到 3.x 的轉換
系列文
重啟挑戰:老派軟體工程師的測試修練27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言