{"id":"12ee9b5b-559e-4037-9408-ed86b4fa0607","shortId":"KPKURa","kind":"skill","title":"dotnet-testing-unit-test-fundamentals","tagline":".NET 單元測試基礎與 FIRST 原則的專門技能。當需要建立單元測試、了解測試基礎、學習 3A Pattern、掌握測試最佳實踐時使用。涵蓋 FIRST 原則、AAA Pattern、Fact/Theory、測試金字塔等。\nMake 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","description":"# .NET 單元測試基礎指南\n\n## FIRST 原則\n\n好的單元測試遵循以下原則，因為這些原則能確保測試的可靠性與維護性：\n\n### F - Fast (快速)\n\n測試執行時間應在毫秒級，不依賴外部資源。\n\n```csharp\n[Fact] // Fast: 不依賴外部資源，執行快速\npublic void Add_輸入1和2_應回傳3()\n{\n    // 純記憶體運算，無 I/O 或網路延遲\n    var calculator = new Calculator();\n    var result = calculator.Add(1, 2);\n    Assert.Equal(3, result);\n}\n```\n\n### I - Independent (獨立)\n\n測試之間不應有相依性，每個測試都建立新的實例。\n\n```csharp\n[Fact] // Independent: 每個測試都建立新的實例\npublic void Increment_從0開始_應回傳1()\n{\n    var counter = new Counter(); // 每個測試都建立新的實例，不受其他測試影響\n    counter.Increment();\n    Assert.Equal(1, counter.Value);\n}\n```\n\n### R - Repeatable (可重複)\n\n在任何環境都能得到相同結果，不依賴外部狀態。\n\n```csharp\n[Fact] // Repeatable: 每次執行都得到相同結果\npublic void Increment_多次執行_應產生一致結果()\n{\n    var counter = new Counter();\n    counter.Increment();\n    counter.Increment();\n    counter.Increment();\n    \n    // 每次執行這個測試都會得到相同結果\n    Assert.Equal(3, counter.Value);\n}\n```\n\n### S - Self-Validating (自我驗證)\n\n測試結果應為明確的通過或失敗，使用清晰的斷言。\n\n```csharp\n[Fact] // Self-Validating: 明確的驗證\npublic void IsValidEmail_輸入有效Email_應回傳True()\n{\n    var emailHelper = new EmailHelper();\n    var result = emailHelper.IsValidEmail(\"test@example.com\");\n    \n    Assert.True(result); // 明確的通過或失敗\n}\n```\n\n### T - Timely (及時)\n\n測試應在產品程式碼之前或同時撰寫，確保程式碼的可測試性。\n\n## 3A Pattern 結構\n\n每個測試方法遵循 Arrange-Act-Assert 模式，這種結構讓測試意圖一目了然：\n\n```csharp\n[Fact]\npublic void Add_輸入負數和正數_應回傳正確結果()\n{\n    // Arrange - 準備測試資料與相依物件\n    var calculator = new Calculator();\n    const int a = -5;\n    const int b = 3;\n    const int expected = -2;\n\n    // Act - 執行被測試的方法\n    var result = calculator.Add(a, b);\n\n    // Assert - 驗證結果是否符合預期\n    Assert.Equal(expected, result);\n}\n```\n\n### 各區塊職責\n\n| 區塊        | 職責                           | 注意事項                            |\n| ----------- | ------------------------------ | ----------------------------------- |\n| **Arrange** | 準備測試所需的物件、資料、Mock | 使用 `const` 宣告常數值，提高可讀性 |\n| **Act**     | 執行被測試的方法               | 通常只有一行，呼叫被測方法          |\n| **Assert**  | 驗證結果                       | 每個測試只驗證一個行為              |\n\n## 測試命名規範\n\n使用以下格式命名測試方法：\n\n```text\n[被測試方法名稱]_[測試情境]_[預期行為]\n```\n\n### 命名範例\n\n| 方法名稱                                       | 說明         |\n| ---------------------------------------------- | ------------ |\n| `Add_輸入1和2_應回傳3`                         | 測試正常輸入 |\n| `Add_輸入負數和正數_應回傳正確結果`            | 測試邊界條件 |\n| `Divide_輸入10和0_應拋出DivideByZeroException` | 測試例外情況 |\n| `IsValidEmail_輸入null值_應回傳False`          | 測試無效輸入 |\n| `GetDomain_輸入有效Email_應回傳網域名稱`       | 測試回傳值   |\n\n> **提示**：使用中文命名可以讓測試報告更易讀，特別是在團隊溝通時。\n\n## xUnit 測試屬性\n\n### [Fact] - 單一測試案例\n\n用於測試單一情境：\n\n```csharp\n[Fact]\npublic void Add_輸入0和0_應回傳0()\n{\n    var calculator = new Calculator();\n    var result = calculator.Add(0, 0);\n    Assert.Equal(0, result);\n}\n```\n\n### [Theory] + [InlineData] - 參數化測試\n\n用於測試多個輸入組合：\n\n```csharp\n[Theory]\n[InlineData(1, 2, 3)]\n[InlineData(-1, 1, 0)]\n[InlineData(0, 0, 0)]\n[InlineData(100, -50, 50)]\npublic void Add_輸入各種數值組合_應回傳正確結果(int a, int b, int expected)\n{\n    var calculator = new Calculator();\n    var result = calculator.Add(a, b);\n    Assert.Equal(expected, result);\n}\n```\n\n### 測試多個無效輸入\n\n```csharp\n[Theory]\n[InlineData(\"invalid-email\")]\n[InlineData(\"@example.com\")]\n[InlineData(\"test@\")]\n[InlineData(\"test.example.com\")]\npublic void IsValidEmail_輸入無效Email格式_應回傳False(string invalidEmail)\n{\n    var emailHelper = new EmailHelper();\n    var result = emailHelper.IsValidEmail(invalidEmail);\n    Assert.False(result);\n}\n```\n\n## 例外測試\n\n測試預期會拋出例外的情況：\n\n```csharp\n[Fact]\npublic void Divide_輸入10和0_應拋出DivideByZeroException()\n{\n    // Arrange\n    var calculator = new Calculator();\n    const decimal dividend = 10m;\n    const decimal divisor = 0m;\n\n    // Act & Assert\n    var exception = Assert.Throws<DivideByZeroException>(\n        () => calculator.Divide(dividend, divisor)\n    );\n\n    // 驗證例外訊息\n    Assert.Equal(\"除數不能為零\", exception.Message);\n}\n```\n\n## 測試專案結構\n\n建議的專案結構：\n\n```text\nSolution/\n├── src/\n│   └── MyProject/\n│       ├── Calculator.cs\n│       └── MyProject.csproj\n└── tests/\n    └── MyProject.Tests/\n        ├── CalculatorTests.cs\n        └── MyProject.Tests.csproj\n```\n\n## 測試專案範本 (.csproj)\n\n```xml\n<Project Sdk=\"Microsoft.NET.Sdk\">\n\n    <PropertyGroup>\n        <TargetFramework>net9.0</TargetFramework>\n        <ImplicitUsings>enable</ImplicitUsings>\n        <Nullable>enable</Nullable>\n        <IsPackable>false</IsPackable>\n    </PropertyGroup>\n\n    <ItemGroup>\n        <PackageReference Include=\"coverlet.collector\" Version=\"8.0.1\">\n            <PrivateAssets>all</PrivateAssets>\n            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n        </PackageReference>\n        <PackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"18.3.0\" />\n        <PackageReference Include=\"xunit\" Version=\"2.9.3\" />\n        <PackageReference Include=\"xunit.runner.visualstudio\" Version=\"3.1.5\">\n            <PrivateAssets>all</PrivateAssets>\n            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n        </PackageReference>\n    </ItemGroup>\n\n    <ItemGroup>\n        <Using Include=\"Xunit\" />\n    </ItemGroup>\n\n    <ItemGroup>\n        <ProjectReference Include=\"..\\..\\src\\MyProject\\MyProject.csproj\" />\n    </ItemGroup>\n\n</Project>\n```\n\n## 常用斷言方法\n\n| 斷言方法                            | 用途             |\n| ----------------------------------- | ---------------- |\n| `Assert.Equal(expected, actual)`    | 驗證相等         |\n| `Assert.NotEqual(expected, actual)` | 驗證不相等       |\n| `Assert.True(condition)`            | 驗證條件為真     |\n| `Assert.False(condition)`           | 驗證條件為假     |\n| `Assert.Null(object)`               | 驗證為 null      |\n| `Assert.NotNull(object)`            | 驗證不為 null    |\n| `Assert.Throws<T>(action)`          | 驗證拋出特定例外 |\n| `Assert.Empty(collection)`          | 驗證集合為空     |\n| `Assert.Contains(item, collection)` | 驗證集合包含項目 |\n\n## 生成測試的檢查清單\n\n為方法生成測試時，請確保涵蓋：\n\n- [ ] **正常路徑** - 標準輸入產生預期輸出\n- [ ] **邊界條件** - 最小值、最大值、零、空字串\n- [ ] **無效輸入** - null、負數、格式錯誤\n- [ ] **例外情況** - 預期會拋出例外的情境\n\n## 輸出格式\n\n- 產生符合 FIRST 原則的 xUnit 測試類別（.cs 檔案）\n- 每個測試方法遵循 AAA Pattern（Arrange-Act-Assert）\n- 使用三段式命名法（方法_情境_預期）\n- 涵蓋正常路徑、邊界條件、無效輸入、例外情況\n\n## 參考資源\n\n### 原始文章\n\n本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章：\n\n- **Day 01 - 老派工程師的測試啟蒙**\n  - 鐵人賽文章：https://ithelp.ithome.com.tw/articles/10373888\n  - 範例程式碼：https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day01\n\n### 相關技能\n\n- `dotnet-testing-test-naming-conventions` - 測試命名規範\n- `dotnet-testing-xunit-project-setup` - xUnit 專案設定\n- `dotnet-testing-awesome-assertions-guide` - 流暢斷言","tags":["dotnet","testing","unit","test","fundamentals","agent","skills","kevintsengtw","agent-skills","ai-assisted-development","copilot-skills","csharp"],"capabilities":["skill","source-kevintsengtw","skill-dotnet-testing-unit-test-fundamentals","topic-agent-skills","topic-ai-assisted-development","topic-copilot-skills","topic-csharp","topic-dotnet","topic-dotnet-testing","topic-github-copilot","topic-integration-testing","topic-testing","topic-unit-testing","topic-xunit"],"categories":["dotnet-testing-agent-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/kevintsengtw/dotnet-testing-agent-skills/dotnet-testing-unit-test-fundamentals","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add kevintsengtw/dotnet-testing-agent-skills","source_repo":"https://github.com/kevintsengtw/dotnet-testing-agent-skills","install_from":"skills.sh"}},"qualityScore":"0.461","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 23 github stars · SKILL.md body (6,495 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-24T13:02:27.542Z","embedding":null,"createdAt":"2026-04-18T23:04:35.699Z","updatedAt":"2026-04-24T13:02:27.542Z","lastSeenAt":"2026-04-24T13:02:27.542Z","tsv":"'-1':305 '-2':206 '-5':198 '-50':314 '/articles/10373888':523 '/kevintsengtw/30days_in_testing_samples/tree/main/day01':527 '0':289,290,292,307,309,310,311 '01':518 '0m':390 '1':84,111,301,306 '100':313 '10m':386 '2':85,302 '3':87,136,202,303 '30':514 '3a':14,172 '50':315 'aaa':20,496 'aaa/3a':39 'act':178,207,231,391,500 'action':462 'actual':441,445 'add':70,186,247,251,279,318 'analyz':427,434 'arrang':177,189,223,378,499 'arrange-act-assert':176,498 'assert':179,214,235,392,501,548 'assert.contains':467 'assert.empty':464 'assert.equal':86,110,135,216,291,336,400,439 'assert.false':367,450 'assert.notequal':443 'assert.notnull':457 'assert.null':453 'assert.throws':395,461 'assert.true':164,447 'awesom':547 'b':201,213,324,335 'basic':48 'build':424,431 'buildtransit':428,435 'calcul':78,80,192,194,283,285,328,330,380,382 'calculator.add':83,211,288,333 'calculator.cs':409 'calculator.divide':396 'calculatortests.cs':413 'collect':465,469 'condit':448,451 'const':195,199,203,228,383,387 'contentfil':426,433 'convent':534 'counter':104,106,128,130 'counter.increment':109,131,132,133 'counter.value':112,137 'cs':493 'csharp':63,94,118,145,182,275,298,340,371 'csproj':416 'day':517 'decim':384,388 'divid':255,375 'dividend':385,397 'divisor':389,398 'dotnet':2,530,537,545 'dotnet-testing-awesome-assertions-guid':544 'dotnet-testing-test-naming-convent':529 'dotnet-testing-unit-test-fundament':1 'dotnet-testing-xunit-project-setup':536 'e':51 'email':345 'emailhelp':157,159,360,362 'emailhelper.isvalidemail':162,365 'enabl':419,420 'example.com':347 'except':394 'exception.message':402 'expect':205,217,326,337,440,444 'f':58 'fact':64,95,119,146,183,272,276,372 'fact/theory':22 'fals':421 'fast':59,65 'first':9,18,37,54,489 'fundament':6,36 'getdomain':263 'github.com':526 'github.com/kevintsengtw/30days_in_testing_samples/tree/main/day01':525 'guid':549 'i/o':75 'increment':100,124 'independ':90,96 'inlinedata':295,300,304,308,312,342,346,348,350 'int':196,200,204,321,323,325 'invalid':344 'invalid-email':343 'invalidemail':358,366 'isvalidemail':153,259,354 'item':468 'ithelp.ithome.com.tw':522 'ithelp.ithome.com.tw/articles/10373888':521 'learn':44 'make':24 'mention':33 'mock':226 'myproject':408 'myproject.csproj':410 'myproject.tests':412 'myproject.tests.csproj':414 'name':533 'nativ':425,432 'net':7,49,52 'net9.0':418 'new':79,105,129,158,193,284,329,361,381 'null':456,460,482 'object':454,458 'pattern':15,21,40,173,497 'principl':38 'project':540 'public':68,98,122,151,184,277,316,352,373 'r':113 'repeat':114,120 'result':82,88,161,165,210,218,287,293,332,338,364,368 'runtim':423,430 'self':140,148 'self-valid':139,147 'setup':541 'skill':29 'skill-dotnet-testing-unit-test-fundamentals' 'solut':406 'source-kevintsengtw' 'src':407 'string':357 'sure':25 'test':3,5,35,50,349,411,531,532,538,546 'test.example.com':351 'test@example.com':163 'text':240,405 'theori':294,299,341 'time':168 'topic-agent-skills' 'topic-ai-assisted-development' 'topic-copilot-skills' 'topic-csharp' 'topic-dotnet' 'topic-dotnet-testing' 'topic-github-copilot' 'topic-integration-testing' 'topic-testing' 'topic-unit-testing' 'topic-xunit' 'unit':4,34 'use':27 'user':32 'valid':141,149 'var':77,81,103,127,156,160,191,209,282,286,327,331,359,363,379,393 'void':69,99,123,152,185,278,317,353,374 'want':42 'whenev':30 'write':47 'xml':417 'xunit':270,491,539,542 '不依賴外部狀態':117 '不依賴外部資源':62,66 '不受其他測試影響':108 '了解測試基礎':12 '使用':227 '使用三段式命名法':502 '使用中文命名可以讓測試報告更易讀':268 '使用以下格式命名測試方法':239 '使用清晰的斷言':144 '例外情況':485,509 '例外測試':369 '區塊':220 '原則':19,55 '原則的':490 '原則的專門技能':10 '原始文章':511 '參數化測試':296 '參考資源':510 '及時':169 '可重複':115 '各區塊職責':219 '呼叫被測方法':234 '命名範例':244 '單一測試案例':273 '單元測試基礎指南':53 '單元測試基礎與':8 '因為這些原則能確保測試的可靠性與維護性':57 '在任何環境都能得到相同結果':116 '執行快速':67 '執行被測試的方法':208,232 '多次執行':125 '天挑戰':515 '好的單元測試遵循以下原則':56 '學習':13 '宣告常數值':229 '專案設定':543 '常用斷言方法':436 '建議的專案結構':404 '從0開始':101 '快速':60 '情境':504 '應回傳0':281 '應回傳1':102 '應回傳3':72,249 '應回傳fals':261,356 '應回傳true':155 '應回傳正確結果':188,253,320 '應回傳網域名稱':265 '應拋出dividebyzeroexcept':257,377 '應產生一致結果':126 '或網路延遲':76 '掌握測試最佳實踐時使用':16 '提示':267 '提高可讀性':230 '斷言方法':437 '方法':503 '方法名稱':245 '明確的通過或失敗':166 '明確的驗證':150 '最大值':478 '最小值':477 '本技能內容提煉自':512 '格式錯誤':484 '標準輸入產生預期輸出':475 '模式':180 '檔案':494 '正常路徑':474 '每個測試只驗證一個行為':237 '每個測試方法遵循':175,495 '每個測試都建立新的實例':93,97,107 '每次執行這個測試都會得到相同結果':134 '每次執行都得到相同結果':121 '注意事項':222 '流暢斷言':550 '涵蓋':17 '涵蓋正常路徑':506 '測試之間不應有相依性':92 '測試例外情況':258 '測試命名規範':238,535 '測試回傳值':266 '測試執行時間應在毫秒級':61 '測試多個無效輸入':339 '測試專案範本':415 '測試專案結構':403 '測試屬性':271 '測試情境':242 '測試應在產品程式碼之前或同時撰寫':170 '測試正常輸入':250 '測試無效輸入':262 '測試結果應為明確的通過或失敗':143 '測試邊界條件':254 '測試金字塔等':23 '測試預期會拋出例外的情況':370 '測試類別':492 '準備測試所需的物件':224 '準備測試資料與相依物件':190 '為方法生成測試時':472 '無':74 '無效輸入':481,508 '特別是在團隊溝通時':269 '獨立':91 '生成測試的檢查清單':471 '產生符合':488 '用於測試單一情境':274 '用於測試多個輸入組合':297 '用途':438 '當需要建立單元測試':11 '相關技能':528 '確保程式碼的可測試性':171 '空字串':480 '範例程式碼':524 '系列文章':516 '純記憶體運算':73 '結構':174 '老派工程師的測試啟蒙':519 '老派軟體工程師的測試修練':513 '職責':221 '自我驗證':142 '被測試方法名稱':241 '說明':246 '請確保涵蓋':473 '負數':483 '資料':225 '輸入0和0':280 '輸入10和0':256,376 '輸入1和2':71,248 '輸入null值':260 '輸入各種數值組合':319 '輸入有效email':154,264 '輸入無效email格式':355 '輸入負數和正數':187,252 '輸出格式':487 '這種結構讓測試意圖一目了然':181 '通常只有一行':233 '邊界條件':476,507 '鐵人賽文章':520 '除數不能為零':401 '零':479 '預期':505 '預期會拋出例外的情境':486 '預期行為':243 '驗證不為':459 '驗證不相等':446 '驗證例外訊息':399 '驗證拋出特定例外':463 '驗證條件為假':452 '驗證條件為真':449 '驗證為':455 '驗證相等':442 '驗證結果':236 '驗證結果是否符合預期':215 '驗證集合包含項目':470 '驗證集合為空':466","prices":[{"id":"388eac5b-586f-4a17-a513-5f9e0d826b34","listingId":"12ee9b5b-559e-4037-9408-ed86b4fa0607","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"kevintsengtw","category":"dotnet-testing-agent-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T23:04:35.699Z"}],"sources":[{"listingId":"12ee9b5b-559e-4037-9408-ed86b4fa0607","source":"github","sourceId":"kevintsengtw/dotnet-testing-agent-skills/dotnet-testing-unit-test-fundamentals","sourceUrl":"https://github.com/kevintsengtw/dotnet-testing-agent-skills/tree/main/skills/dotnet-testing-unit-test-fundamentals","isPrimary":false,"firstSeenAt":"2026-04-18T23:04:35.699Z","lastSeenAt":"2026-04-24T13:02:27.542Z"}],"details":{"listingId":"12ee9b5b-559e-4037-9408-ed86b4fa0607","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"kevintsengtw","slug":"dotnet-testing-unit-test-fundamentals","github":{"repo":"kevintsengtw/dotnet-testing-agent-skills","stars":23,"topics":["agent-skills","ai-assisted-development","copilot-skills","csharp","dotnet","dotnet-testing","github-copilot","integration-testing","testing","unit-testing","xunit"],"license":"mit","html_url":"https://github.com/kevintsengtw/dotnet-testing-agent-skills","pushed_at":"2026-03-31T07:28:56Z","description":"AI Agent Skills for .NET Testing - Based on 30-Day Testing Challenge (iThome Ironman 2025 Winner)","skill_md_sha":"0e53c05d2c0226a2ad11bd1cf3cfd512f1f9c0ec","skill_md_path":"skills/dotnet-testing-unit-test-fundamentals/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/kevintsengtw/dotnet-testing-agent-skills/tree/main/skills/dotnet-testing-unit-test-fundamentals"},"layout":"multi","source":"github","category":"dotnet-testing-agent-skills","frontmatter":{"name":"dotnet-testing-unit-test-fundamentals","description":".NET 單元測試基礎與 FIRST 原則的專門技能。當需要建立單元測試、了解測試基礎、學習 3A Pattern、掌握測試最佳實踐時使用。涵蓋 FIRST 原則、AAA Pattern、Fact/Theory、測試金字塔等。\nMake 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, even if they don't explicitly ask for fundamentals guidance.\nKeywords: unit test, 單元測試, unit testing, test fundamentals, 測試基礎, FIRST principle, FIRST 原則, 3A pattern, AAA pattern, Arrange Act Assert, Fact, Theory, InlineData, 如何寫測試, testing best practices, 建立單元測試"},"skills_sh_url":"https://skills.sh/kevintsengtw/dotnet-testing-agent-skills/dotnet-testing-unit-test-fundamentals"},"updatedAt":"2026-04-24T13:02:27.542Z"}}