在這個 AI 革命的時代,我們如何讓人工智慧成為測試開發的最佳夥伴?
每當我們坐在電腦前準備寫測試時,是否曾經有過這樣的想法:「又要寫重複的測試程式碼了」、「這個邊界條件我可能會忘記測試」、「Mock 物件的設定總是讓我頭痛」?
傳統的測試開發確實充滿挑戰:測試案例的重複性高、邊界條件容易遺漏、Mock 物件設定繁瑣、新手往往需要較長時間才能掌握。但現在,GitHub Copilot 的出現為我們帶來了全新的解決方案。
今天我們將深入探討如何善用 GitHub Copilot 來革新測試開發流程,從基礎設定到進階應用,打造一個高效的測試開發環境。
GitHub Copilot 最直接的貢獻就是大幅提升開發效率。想像一下,當你建立一個新的服務類別時,GitHub Copilot 可以:
實際效果:原本需要 30 分鐘手動撰寫的測試程式碼,現在可能只需要 5-10 分鐘就能完成基礎架構,讓你有更多時間專注在測試邏輯的設計上。
GitHub Copilot 基於大量的開源程式碼訓練,具備識別常見測試模式的能力:
每個團隊都有自己的測試規範和最佳實踐,GitHub Copilot 可以學習並複製這些模式:
對於測試新手來說,GitHub Copilot 就像一位經驗豐富的導師:
為了有效運用 GitHub Copilot,我們需要了解它的能力邊界:
// GitHub Copilot 擅長根據方法簽名產生測試
public decimal CalculateDiscount(decimal price, decimal discountRate)
{
// GitHub Copilot 會建議測試:正常情況、零值、負值、邊界值等
}
// GitHub Copilot 能夠識別依賴並建議 Mock 設定
public class UserService
{
private readonly IUserRepository _repository;
private readonly IEmailService _emailService;
// GitHub Copilot 會建議如何 Mock 這些依賴
}
GitHub Copilot 無法完全理解你的業務需求和領域邏輯,因此:
AI 產生的程式碼並非完美:
雖然 GitHub Copilot 能產生很多測試,但測試品質和覆蓋率仍需人工把關:
要發揮 GitHub Copilot 的最大效用,關鍵在於明確分工:
想要讓 GitHub Copilot 成為你的專屬測試助手,關鍵在於正確的客製化設定。透過建立專案特定的 Instructions 檔案和 Prompt 範本,我們可以讓 GitHub Copilot 理解並遵循團隊的測試規範。
為了達到最佳的測試開發體驗,我建議建立一個三層級的設定架構:
這是整個專案的基礎設定檔案,定義了專案層級的測試規範:
# 專案測試開發指導原則
## 技術棧資訊
- .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() 進行驗證
針對不同類型的測試建立專門的設定檔案:
針對單元測試建立專門的設定檔案 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`
除了 Instructions 檔案,我們還可以建立一套標準化的 Prompt 範本,讓團隊成員能夠快速產生高品質的測試程式碼。
為以下 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() 驗證方法呼叫
- 檢查傳入參數的正確性
- 驗證呼叫次數
類別依賴:[列出所有依賴介面]
要充分發揮 GitHub Copilot 的能力,正確的 VS Code 設定是必不可少的。
在 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"
}
]
}
在專案根目錄的 .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 需要在測試專案檔案中加入特定設定:
<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.runner.json
來設定測試執行行為:
{
"parallelAlgorithm": "conservative",
"maxParallelThreads": 4,
"diagnosticMessages": true,
"failSkips": false,
"preEnumerateTheories": false
}
有了正確的設定基礎,現在我們來學習如何撰寫有效的提示 (Prompt),讓 GitHub Copilot 產生符合需求的測試程式碼。
一個有效的測試產生提示應該包含以下四個核心要素:
1. 明確指定測試框架和工具
2. 描述測試範圍和重點
3. 指定預期的測試模式
4. 要求特定的斷言風格
請為以下 C# 類別建立 xUnit 測試類別,具體要求:
【框架規範】
- 測試框架:xUnit v3 (3.0.1)
- Mock 框架:NSubstitute
- 斷言套件:AwesomeAssertions (使用 Should() 語法)
【測試範圍】
- 測試所有公開方法
- 涵蓋正常情況、邊界條件、異常情況
- 特別注意 null 值和空字串的處理
【結構要求】
- 3A 模式並標註 // Arrange, // Act, // Assert
- 命名規範:方法名_測試情境_預期結果
- 使用 [Theory] 和 [InlineData] 處理多個測試案例
【品質標準】
- 每個外部依賴都要建立 Mock
- 斷言要包含描述性訊息
- 測試方法要有完整的 XML 註解
[在此貼上要測試的類別程式碼]
以下提供十個經過實戰驗證的 Prompt 範本,幫助你快速產生高品質的測試程式碼。
Ctrl+Shift+I
(Windows) 或 Cmd+Shift+I
(Mac)[參數]
替換為實際的程式碼內容Ctrl+I
(Windows) 或 Cmd+I
(Mac)/tests
命令加上自訂範本內容範例使用流程:
1. 選取 CalculateDiscount 方法
2. 開啟 Chat 並輸入:「使用範本 1 產生基礎單元測試」
3. 貼上範本內容並將 [方法簽名] 替換為實際方法
4. GitHub Copilot 產生完整的測試類別
產生基礎單元測試,包含:
- 正常輸入的測試案例
- 使用真實的測試資料
- 簡潔明確的斷言
- 標準的 3A 結構
目標方法:[方法簽名]
專注於邊界條件測試,涵蓋:
- null、空字串、空集合
- 數值的最大值、最小值、零值
- 字串長度的邊界情況
- 日期時間的特殊值
使用參數化測試來組織這些案例。
建立異常處理測試,確保:
- 使用 Should().Throw<ExceptionType>() 語法
- 驗證異常訊息的內容
- 測試所有可能觸發異常的條件
- 檢查異常後系統狀態的正確性
為非同步方法建立測試,注意:
- 使用 async/await 語法
- 測試 Task 和 Task<T> 的回傳
- 處理 CancellationToken 的情況
- 驗證非同步操作的完成狀態
方法簽名:[async 方法]
建立包含依賴注入的測試:
- 使用 NSubstitute 建立所有依賴的 Mock
- 設定 Mock 物件的預期行為
- 驗證依賴方法的呼叫次數和參數
- 測試依賴服務異常時的處理
類別依賴:[列出依賴清單]
專門測試資料驗證邏輯:
- 有效資料的通過情況
- 各種無效資料的拒絕情況
- 驗證錯誤訊息的準確性
- 複合驗證規則的測試
驗證規則:[描述驗證邏輯]
測試集合相關的操作:
- 空集合的處理
- 單一元素和多元素的情況
- 集合轉換和過濾的正確性
- 使用 Should().BeEquivalentTo() 比較集合內容
操作類型:[描述集合操作]
測試物件狀態變更:
- 檢查初始狀態
- 驗證操作後的狀態變化
- 確保狀態變更的原子性
- 測試狀態變更的副作用
狀態變更邏輯:[描述狀態變更]
建立效能相關的測試:
- 測試大數據量的處理能力
- 驗證超時機制的運作
- 檢查記憶體使用是否合理
- 使用 StopWatch 測量執行時間
效能要求:[描述效能標準]
建立整合測試案例:
- 使用真實的外部依賴
- 設定完整的測試環境
- 測試端到端的業務流程
- 包含清理和重置邏輯
整合範圍:[描述整合邊界]
描述策略要點:
範例描述:
測試使用者註冊的正常流程:
- 輸入:有效的 Email (user@example.com) 和強密碼 (SecurePass123!)
- 預期:成功建立使用者帳戶,回傳使用者ID,發送歡迎郵件
- 驗證:資料庫記錄、郵件服務呼叫、回傳值格式
重點關注的邊界:
測試計算折扣的邊界條件:
測試使用者名稱驗證:
測試批次處理功能:
系統性的異常測試策略:
測試所有無效輸入組合:
- null 參數:ArgumentNullException
- 空字串參數:ArgumentException
- 格式錯誤:FormatException
- 範圍錯誤:ArgumentOutOfRangeException
模擬外部依賴的各種失敗情況:
- 網路連線失敗:HttpRequestException
- 服務暫時不可用:ServiceUnavailableException
- 認證失敗:UnauthorizedException
- 資料不一致:InvalidOperationException
GitHub Copilot 提供了多種方式來快速初始化測試環境,包括 /tests
指令和自然語言提示。
/tests
指令產生測試最直接的方式是使用 /tests
指令來為選取的程式碼產生測試:
/tests
指令對於更複雜的測試環境設定,使用詳細的自然語言提示效果更好:
請幫我建立一個 xUnit v3 測試專案,包含:
- NSubstitute Mock 框架
- AwesomeAssertions 斷言套件
- 基本的測試類別結構
- GlobalUsings.cs 檔案
- 適當的 .csproj 設定
為 UserService 類別建立完整的測試套件:
- 覆蓋所有公開方法
- 包含正常情況、邊界條件、異常情況測試
- 使用 NSubstitute 建立 Mock 物件
- 目標覆蓋率 90% 以上
- 遵循 3A 模式並加上註解
建立整合測試環境設定:
- 使用 WebApplicationFactory 建立測試伺服器
- 設定 TestContainers 用於資料庫測試
- 包含測試資料的初始化和清理
- 建立基礎測試類別供其他測試繼承
啟用 GitHub Copilot 的 Agent Mode 可以讓 AI 主動執行多個步驟:
在 Agent Mode 中輸入:
請設定一個完整的 .NET 9 測試專案,包含 xUnit v3、NSubstitute 和 AwesomeAssertions,並建立適當的專案結構。
Agent Mode 會自動:
# 確保專案已安裝必要套件
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();
}
}
當你熟悉了基本的測試產生技巧後,是時候探索 GitHub Copilot 的進階功能,讓 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
// ...
}
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!"
};
}
}
分析以下測試程式碼的可讀性問題,並提供改善建議:
重點檢查:
1. 測試意圖是否清楚
2. 測試資料是否有意義
3. 斷言是否具體明確
4. 註解是否必要且有幫助
[貼上需要檢查的測試程式碼]
// 改善前:意圖不明確
[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);
}
GitHub Copilot 能夠分析類別的依賴關係,並自動建議完整的 Mock 設定:
分析以下類別的依賴關係,並產生完整的 Mock 設定:
要求:
1. 識別所有需要 Mock 的依賴介面
2. 為每個依賴建立適當的 Mock 物件
3. 設定常見情境的回傳值和行為
4. 包含異常情況的模擬
[貼上類別程式碼]
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 = "信用卡餘額不足" });
}
}
[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}");
}
}
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
設定範例:
[*.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"
這個腳本幫助開發者在本機就能確保測試命名符合團隊規範,避免在程式碼審查階段才發現問題。
讓我們通過一個完整的範例來展示如何運用本文所學的技術。
# 建立專案目錄
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
建立 .github/copilot-instructions.md
:
# 使用者管理系統測試指導
## 技術棧
- .NET 9, xUnit v3, NSubstitute, AwesomeAssertions
## 業務規則
- 使用者註冊需要驗證 Email 唯一性
- 密碼必須符合複雜度要求(8-50字元,包含數字和字母)
- 使用者狀態:Active, Inactive, Suspended
## 測試重點
- Email 格式和唯一性驗證
- 密碼安全性檢查
- 使用者狀態轉換邏輯
- 異常處理的完整性
對 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 後的效果:
GitHub Copilot 正在改變我們開發測試的方式。透過適當的設定和技巧,它不僅能大幅提升開發效率,更能幫助我們建立更完整、更一致的測試套件。
但請記住,AI 是工具,不是替代品。最好的測試仍然需要人類的創意、經驗和對業務的深度理解。讓我們善用 AI 的優勢,專注在更有價值的測試策略設計和品質把關上。
未來的測試開發將是人機協作的新模式。掌握這些技能,你就能在這個變革中保持領先地位。
這是「重啟挑戰:老派軟體工程師的測試修練」的第二十七天。明天會介紹 Day 28 – TUnit 入門:下世代 .NET 測試框架探索。