$npx -y skills add kevintsengtw/dotnet-testing-agent-skills --skill dotnet-testing-unit-test-fundamentals.NET 單元測試基礎與 FIRST 原則的專門技能。當需要建立單元測試、了解測試基礎、學習 3A Pattern、掌握測試最佳實踐時使用。涵蓋 FIRST 原則、AAA Pattern、Fact/Theory、測試金字塔等。 Make sure to use this skill whenever the user mentions unit testing fundamentals, FIRST principles, AAA/3A pattern, or wants to learn how to write basic .NET tests, e
| 1 | # .NET 單元測試基礎指南 |
| 2 | |
| 3 | ## FIRST 原則 |
| 4 | |
| 5 | 好的單元測試遵循以下原則,因為這些原則能確保測試的可靠性與維護性: |
| 6 | |
| 7 | ### F - Fast (快速) |
| 8 | |
| 9 | 測試執行時間應在毫秒級,不依賴外部資源。 |
| 10 | |
| 11 | ```csharp |
| 12 | [Fact] // Fast: 不依賴外部資源,執行快速 |
| 13 | public void Add_輸入1和2_應回傳3() |
| 14 | { |
| 15 | // 純記憶體運算,無 I/O 或網路延遲 |
| 16 | var calculator = new Calculator(); |
| 17 | var result = calculator.Add(1, 2); |
| 18 | Assert.Equal(3, result); |
| 19 | } |
| 20 | ``` |
| 21 | |
| 22 | ### I - Independent (獨立) |
| 23 | |
| 24 | 測試之間不應有相依性,每個測試都建立新的實例。 |
| 25 | |
| 26 | ```csharp |
| 27 | [Fact] // Independent: 每個測試都建立新的實例 |
| 28 | public void Increment_從0開始_應回傳1() |
| 29 | { |
| 30 | var counter = new Counter(); // 每個測試都建立新的實例,不受其他測試影響 |
| 31 | counter.Increment(); |
| 32 | Assert.Equal(1, counter.Value); |
| 33 | } |
| 34 | ``` |
| 35 | |
| 36 | ### R - Repeatable (可重複) |
| 37 | |
| 38 | 在任何環境都能得到相同結果,不依賴外部狀態。 |
| 39 | |
| 40 | ```csharp |
| 41 | [Fact] // Repeatable: 每次執行都得到相同結果 |
| 42 | public void Increment_多次執行_應產生一致結果() |
| 43 | { |
| 44 | var counter = new Counter(); |
| 45 | counter.Increment(); |
| 46 | counter.Increment(); |
| 47 | counter.Increment(); |
| 48 | |
| 49 | // 每次執行這個測試都會得到相同結果 |
| 50 | Assert.Equal(3, counter.Value); |
| 51 | } |
| 52 | ``` |
| 53 | |
| 54 | ### S - Self-Validating (自我驗證) |
| 55 | |
| 56 | 測試結果應為明確的通過或失敗,使用清晰的斷言。 |
| 57 | |
| 58 | ```csharp |
| 59 | [Fact] // Self-Validating: 明確的驗證 |
| 60 | public void IsValidEmail_輸入有效Email_應回傳True() |
| 61 | { |
| 62 | var emailHelper = new EmailHelper(); |
| 63 | var result = emailHelper.IsValidEmail("test@example.com"); |
| 64 | |
| 65 | Assert.True(result); // 明確的通過或失敗 |
| 66 | } |
| 67 | ``` |
| 68 | |
| 69 | ### T - Timely (及時) |
| 70 | |
| 71 | 測試應在產品程式碼之前或同時撰寫,確保程式碼的可測試性。 |
| 72 | |
| 73 | ## 3A Pattern 結構 |
| 74 | |
| 75 | 每個測試方法遵循 Arrange-Act-Assert 模式,這種結構讓測試意圖一目了然: |
| 76 | |
| 77 | ```csharp |
| 78 | [Fact] |
| 79 | public void Add_輸入負數和正數_應回傳正確結果() |
| 80 | { |
| 81 | // Arrange - 準備測試資料與相依物件 |
| 82 | var calculator = new Calculator(); |
| 83 | const int a = -5; |
| 84 | const int b = 3; |
| 85 | const int expected = -2; |
| 86 | |
| 87 | // Act - 執行被測試的方法 |
| 88 | var result = calculator.Add(a, b); |
| 89 | |
| 90 | // Assert - 驗證結果是否符合預期 |
| 91 | Assert.Equal(expected, result); |
| 92 | } |
| 93 | ``` |
| 94 | |
| 95 | ### 各區塊職責 |
| 96 | |
| 97 | | 區塊 | 職責 | 注意事項 | |
| 98 | | ----------- | ------------------------------ | ----------------------------------- | |
| 99 | | **Arrange** | 準備測試所需的物件、資料、Mock | 使用 `const` 宣告常數值,提高可讀性 | |
| 100 | | **Act** | 執行被測試的方法 | 通常只有一行,呼叫被測方法 | |
| 101 | | **Assert** | 驗證結果 | 每個測試只驗證一個行為 | |
| 102 | |
| 103 | ## 測試命名規範 |
| 104 | |
| 105 | 使用以下格式命名測試方法: |
| 106 | |
| 107 | ```text |
| 108 | [被測試方法名稱]_[測試情境]_[預期行為] |
| 109 | ``` |
| 110 | |
| 111 | ### 命名範例 |
| 112 | |
| 113 | | 方法名稱 | 說明 | |
| 114 | | ---------------------------------------------- | ------------ | |
| 115 | | `Add_輸入1和2_應回傳3` | 測試正常輸入 | |
| 116 | | `Add_輸入負數和正數_應回傳正確結果` | 測試邊界條件 | |
| 117 | | `Divide_輸入10和0_應拋出DivideByZeroException` | 測試例外情況 | |
| 118 | | `IsValidEmail_輸入null值_應回傳False` | 測試無效輸入 | |
| 119 | | `GetDomain_輸入有效Email_應回傳網域名稱` | 測試回傳值 | |
| 120 | |
| 121 | > **提示**:使用中文命名可以讓測試報告更易讀,特別是在團隊溝通時。 |
| 122 | |
| 123 | ## xUnit 測試屬性 |
| 124 | |
| 125 | ### [Fact] - 單一測試案例 |
| 126 | |
| 127 | 用於測試單一情境: |
| 128 | |
| 129 | ```csharp |
| 130 | [Fact] |
| 131 | public void Add_輸入0和0_應回傳0() |
| 132 | { |
| 133 | var calculator = new Calculator(); |
| 134 | var result = calculator.Add(0, 0); |
| 135 | Assert.Equal(0, result); |
| 136 | } |
| 137 | ``` |
| 138 | |
| 139 | ### [Theory] + [InlineData] - 參數化測試 |
| 140 | |
| 141 | 用於測試多個輸入組合: |
| 142 | |
| 143 | ```csharp |
| 144 | [Theory] |
| 145 | [InlineData(1, 2, 3)] |
| 146 | [InlineData(-1, 1, 0)] |
| 147 | [InlineData(0, 0, 0)] |
| 148 | [InlineData(100, -50, 50)] |
| 149 | public void Add_輸入各種數值組合_應回傳正確結果(int a, int b, int expected) |
| 150 | { |
| 151 | var calculator = new Calculator(); |
| 152 | var result = calculator.Add(a, b); |
| 153 | Assert.Equal(expected, result); |
| 154 | } |
| 155 | ``` |
| 156 | |
| 157 | ### 測試多個無效輸入 |
| 158 | |
| 159 | ```csharp |
| 160 | [Theory] |
| 161 | [InlineData("invalid-email")] |
| 162 | [InlineData("@example.com")] |
| 163 | [InlineData("test@")] |
| 164 | [InlineData("test.example.com")] |
| 165 | public void IsValidEmail_輸入無效Email格式_應回傳False(string invalidEmail) |
| 166 | { |
| 167 | var emailHelper = new EmailHelper(); |
| 168 | var result = emailHelper.IsValidEmail(invalidEmail); |
| 169 | Assert.False(result); |
| 170 | } |
| 171 | ``` |
| 172 | |
| 173 | ## 例外測試 |
| 174 | |
| 175 | 測試預期會拋出例外的情況: |
| 176 | |
| 177 | ```csharp |
| 178 | [Fact] |
| 179 | public void Divide_輸入10和0_應拋出DivideByZeroException() |
| 180 | { |
| 181 | // Arrange |
| 182 | var calculator = new Calculator(); |
| 183 | const decimal dividend = 10m; |
| 184 | const decimal divisor = 0m; |
| 185 | |
| 186 | // Act & Assert |
| 187 | var exception = Assert.Throws<DivideByZeroException>( |
| 188 | () => calculator.Divide(dividend, divisor) |
| 189 | ); |
| 190 | |
| 191 | // 驗證例外訊息 |
| 192 | Assert.Equal("除數不能為零", exception.Message); |
| 193 | } |
| 194 | ``` |
| 195 | |
| 196 | ## 測試專案結構 |
| 197 | |
| 198 | 建議的專案結構: |
| 199 | |
| 200 | ```text |
| 201 | Solution/ |
| 202 | ├── src/ |
| 203 | │ └── MyProject/ |
| 204 | │ ├── Calculator.cs |
| 205 | │ └── MyProject.csproj |
| 206 | └── tests/ |
| 207 | └── MyProject.Tests/ |
| 208 | ├── CalculatorTests.cs |
| 209 | └── MyProject.Tests.csproj |
| 210 | ``` |
| 211 | |
| 212 | ## 測試專案範本 (.csproj) |
| 213 | |
| 214 | ```xml |
| 215 | <Project Sdk="Microsoft.NET.Sdk"> |
| 216 | |
| 217 | <PropertyGroup> |
| 218 | <TargetFramework>net9.0</TargetFramework> |
| 219 | <ImplicitUsings>enable</ImplicitUsings> |
| 220 | <Nullable>enable</Nullable> |
| 221 | <IsPackable>false</IsPackable> |
| 222 | </PropertyGroup> |
| 223 | |
| 224 | <ItemGroup> |
| 225 | <PackageReference Include=" |