$npx -y skills add kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-fluentvalidation-testing測試 FluentValidation 驗證器的專門技能。當需要為 Validator 類別建立測試、驗證業務規則、測試錯誤訊息時使用。涵蓋 FluentValidation.TestHelper 完整使用、ShouldHaveValidationErrorFor、非同步驗證、跨欄位邏輯等。 Make sure to use this skill whenever the user mentions FluentValidation testing, Validator testing, ShouldHaveValidationErrorFor, Tes
| 1 | # FluentValidation 驗證器測試指南 |
| 2 | |
| 3 | ## 為什麼要測試驗證器? |
| 4 | |
| 5 | 驗證器是應用程式的第一道防線,測試驗證器能: |
| 6 | |
| 7 | 1. **確保資料完整性** - 防止無效資料進入系統 |
| 8 | 2. **業務規則文件化** - 測試即活文件,清楚展示業務規則 |
| 9 | 3. **安全性保障** - 防止惡意或不當資料輸入 |
| 10 | 4. **重構安全網** - 業務規則變更時提供保障 |
| 11 | 5. **跨欄位邏輯驗證** - 確保複雜邏輯正確運作 |
| 12 | |
| 13 | ## 前置需求 |
| 14 | |
| 15 | ### 套件安裝 |
| 16 | |
| 17 | ```xml |
| 18 | <PackageReference Include="FluentValidation" Version="12.1.1" /> |
| 19 | <PackageReference Include="xunit" Version="2.9.3" /> |
| 20 | <PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="10.4.0" /> |
| 21 | <PackageReference Include="NSubstitute" Version="5.3.0" /> |
| 22 | <PackageReference Include="AwesomeAssertions" Version="9.4.0" /> |
| 23 | ``` |
| 24 | |
| 25 | > **注意**:`FluentValidation.TestHelper` 命名空間(`TestValidate`、`ShouldHaveValidationErrorFor` 等 API)已包含在 `FluentValidation` 主套件中,不需要額外安裝獨立套件。只需 `using FluentValidation.TestHelper;` 即可使用。 |
| 26 | |
| 27 | > **FluentValidation 12.x 注意事項**:FluentValidation 12.0 為主要版本升級,最低需求為 **.NET 8**。已移除的 API 包括 `Transform`/`TransformForEach`(改用 `Must` + 手動轉換)、`InjectValidator`(改用建構子注入 + `SetValidator`)、`CascadeMode.StopOnFirstFailure`(改用 `RuleLevelCascadeMode = CascadeMode.Stop`)。`ShouldHaveAnyValidationError` 已更名為 `ShouldHaveValidationErrors`。完整遷移指南請參閱 [FluentValidation 12.0 Upgrade Guide](https://docs.fluentvalidation.net/en/latest/upgrading-to-12.html)。 |
| 28 | |
| 29 | ### 基本 using 指令 |
| 30 | |
| 31 | ```csharp |
| 32 | using FluentValidation; |
| 33 | using FluentValidation.TestHelper; |
| 34 | using Microsoft.Extensions.Time.Testing; |
| 35 | using NSubstitute; |
| 36 | using Xunit; |
| 37 | using AwesomeAssertions; |
| 38 | ``` |
| 39 | |
| 40 | ## 核心測試模式 |
| 41 | |
| 42 | 本節涵蓋 7 種核心測試模式,每種模式包含驗證器定義與完整測試範例。 |
| 43 | |
| 44 | > 完整程式碼範例請參考 [references/core-test-patterns.md](references/core-test-patterns.md) |
| 45 | |
| 46 | - **模式 1:基本欄位驗證** — 使用 `TestValidate` + `ShouldHaveValidationErrorFor` / `ShouldNotHaveValidationErrorFor` 測試單一欄位規則 |
| 47 | - **模式 2:參數化測試** — 使用 `[Theory]` + `[InlineData]` 測試多種無效/有效輸入組合 |
| 48 | - **模式 3:跨欄位驗證** — 密碼確認、自訂 `Must()` 規則等多欄位關聯驗證 |
| 49 | - **模式 4:時間相依驗證** — 注入 `TimeProvider`,搭配 `FakeTimeProvider` 控制時間進行測試 |
| 50 | - **模式 5:條件式驗證** — 使用 `.When()` 的可選欄位驗證,測試條件觸發與跳過情境 |
| 51 | - **模式 6:非同步驗證** — `MustAsync` + `TestValidateAsync`,搭配 NSubstitute Mock 外部服務 |
| 52 | - **模式 7:集合驗證** — 驗證集合非空與元素有效性 |
| 53 | |
| 54 | ### 快速範例:基本欄位驗證 |
| 55 | |
| 56 | ```csharp |
| 57 | public class UserValidatorTests |
| 58 | { |
| 59 | private readonly UserValidator _validator = new(); |
| 60 | |
| 61 | [Fact] |
| 62 | public void Validate_空白使用者名稱_應該驗證失敗() |
| 63 | { |
| 64 | var result = _validator.TestValidate( |
| 65 | new UserRegistrationRequest { Username = "" }); |
| 66 | |
| 67 | result.ShouldHaveValidationErrorFor(x => x.Username) |
| 68 | .WithErrorMessage("使用者名稱不可為 null 或空白"); |
| 69 | } |
| 70 | } |
| 71 | ``` |
| 72 | |
| 73 | ## FluentValidation.TestHelper 核心 API |
| 74 | |
| 75 | ### 測試方法 |
| 76 | |
| 77 | | 方法 | 用途 | 範例 | |
| 78 | | -------------------------- | -------------- | --------------------------------------------- | |
| 79 | | `TestValidate(model)` | 執行同步驗證 | `_validator.TestValidate(request)` | |
| 80 | | `TestValidateAsync(model)` | 執行非同步驗證 | `await _validator.TestValidateAsync(request)` | |
| 81 | |
| 82 | ### 斷言方法 |
| 83 | |
| 84 | | 方法 | 用途 | 範例 | |
| 85 | | -------------------------------------------------- | ------------------------ | ------------------------------------------------------ | |
| 86 | | `ShouldHaveValidationErrorFor(x => x.Property)` | 斷言該屬性應該有錯誤 | `result.ShouldHaveValidationErrorFor(x => x.Username)` | |
| 87 | | `ShouldNotHaveValidationErrorFor(x => x.Property)` | 斷言該屬性不應該有錯誤 | `result.ShouldNotHaveValidationErrorFor(x => x.Email)` | |
| 88 | | `ShouldNotHaveAnyValidationErrors()` | 斷言整個物件沒有任何錯誤 | `result.ShouldNotHaveAnyValidationErrors()` | |
| 89 | |
| 90 | ### 錯誤訊息驗證 |
| 91 | |
| 92 | | 方法 | 用途 | 範例 | |
| 93 | | -------------------------- | ---------------- | ----------------------------------------- | |
| 94 | | `WithErrorMessage(string)` | 驗證錯誤訊息內容 | `.WithErrorMessage("使用者名稱不可為空")` | |
| 95 | | `WithErrorCode(string)` | 驗證錯誤代碼 | `.WithErrorCode("NOT_EMPTY")` | |
| 96 | |
| 97 | ## 測試最佳實踐 |
| 98 | |
| 99 | ### 推薦做法 |
| 100 | |
| 101 | 1. **使用參數化測試** - 用 Theory 測試多種輸入組合 |
| 102 | 2. **測試邊界值** - 特別注意邊界條件 |
| 103 | 3. **控制時間** - 使用 FakeTimeProvider 處理時間相依 |
| 104 | 4. **Mock 外部依賴** - 使用 NSubstitute 隔離外部服務 |
| 105 | 5. **建立輔助方法** - 統一管理測試資料 |
| 106 | 6. **清楚的測試命名** - 使用 `方法_情境_預期結果` 格式 |
| 107 | 7. **測試錯誤訊息** - 確保使用者看到正確的錯誤訊息 |
| 108 | |
| 109 | ### 避免做法 |
| 110 | |
| 111 | 1. **避免使用 DateTime.Now** - 會導致測試不穩定 |
| 112 | 2. **避免測試過度耦合** - 每個測試只驗證一個規則 |
| 113 | 3. **避免硬編碼測試資料** - 使用輔助方法建立 |
| 114 | 4. **避免忽略邊界條件** - 邊界值是最容易出錯的地方 |
| 115 | 5. **避免跳過錯誤訊息驗證** - 錯誤訊息是使用者體驗的一部分 |
| 116 | |
| 117 | ## 常見測試場景 |
| 118 | |
| 119 | ### 場景 1:Email 格式驗證 |
| 120 | |
| 121 | ```csharp |
| 122 | [Theor |