dotnet-testing-unit-test-fundamentals
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese.NET 單元測試基礎指南
.NET Unit Testing Basics Guide
適用情境
Applicable Scenarios
當被要求執行以下任務時,請使用此技能:
- 為 .NET 類別或方法建立單元測試
- 檢視或改進現有測試的品質
- 設計符合 FIRST 原則的測試案例
- 解釋測試命名規範與最佳實踐
- 使用 xUnit 撰寫測試
Use this skill when you need to perform the following tasks:
- Create unit tests for .NET classes or methods
- Review or improve the quality of existing tests
- Design test cases that comply with FIRST principles
- Explain test naming conventions and best practices
- Write tests using xUnit
FIRST 原則
FIRST Principles
每個單元測試都必須符合以下原則:
Every unit test must comply with the following principles:
F - Fast (快速)
F - Fast
測試執行時間應在毫秒級,不依賴外部資源。
csharp
[Fact] // Fast: 不依賴外部資源,執行快速
public void Add_輸入1和2_應回傳3()
{
// 純記憶體運算,無 I/O 或網路延遲
var calculator = new Calculator();
var result = calculator.Add(1, 2);
Assert.Equal(3, result);
}Tests should execute in milliseconds and not rely on external resources.
csharp
[Fact] // Fast: No dependency on external resources, executes quickly
public void Add_Input1And2_ShouldReturn3()
{
// In-memory operation only, no I/O or network latency
var calculator = new Calculator();
var result = calculator.Add(1, 2);
Assert.Equal(3, result);
}I - Independent (獨立)
I - Independent
測試之間不應有相依性,每個測試都建立新的實例。
csharp
[Fact] // Independent: 每個測試都建立新的實例
public void Increment_從0開始_應回傳1()
{
var counter = new Counter(); // 每個測試都建立新的實例,不受其他測試影響
counter.Increment();
Assert.Equal(1, counter.Value);
}Tests should not depend on each other; each test creates a new instance.
csharp
[Fact] // Independent: Each test creates a new instance
public void Increment_StartFrom0_ShouldReturn1()
{
var counter = new Counter(); // Each test creates a new instance, unaffected by other tests
counter.Increment();
Assert.Equal(1, counter.Value);
}R - Repeatable (可重複)
R - Repeatable
在任何環境都能得到相同結果,不依賴外部狀態。
csharp
[Fact] // Repeatable: 每次執行都得到相同結果
public void Increment_多次執行_應產生一致結果()
{
var counter = new Counter();
counter.Increment();
counter.Increment();
counter.Increment();
// 每次執行這個測試都會得到相同結果
Assert.Equal(3, counter.Value);
}Should produce the same results in any environment, without relying on external state.
csharp
[Fact] // Repeatable: Produces the same result every execution
public void Increment_MultipleExecutions_ShouldProduceConsistentResults()
{
var counter = new Counter();
counter.Increment();
counter.Increment();
counter.Increment();
// This test will produce the same result every time it runs
Assert.Equal(3, counter.Value);
}S - Self-Validating (自我驗證)
S - Self-Validating
測試結果應為明確的通過或失敗,使用清晰的斷言。
csharp
[Fact] // Self-Validating: 明確的驗證
public void IsValidEmail_輸入有效Email_應回傳True()
{
var emailHelper = new EmailHelper();
var result = emailHelper.IsValidEmail("test@example.com");
Assert.True(result); // 明確的通過或失敗
}Test results should clearly pass or fail, using explicit assertions.
csharp
[Fact] // Self-Validating: Explicit verification
public void IsValidEmail_InputValidEmail_ShouldReturnTrue()
{
var emailHelper = new EmailHelper();
var result = emailHelper.IsValidEmail("test@example.com");
Assert.True(result); // Clear pass or fail
}T - Timely (及時)
T - Timely
測試應在產品程式碼之前或同時撰寫,確保程式碼的可測試性。
Tests should be written before or alongside production code to ensure code testability.
3A Pattern 結構
3A Pattern Structure
每個測試方法必須遵循 Arrange-Act-Assert 模式:
csharp
[Fact]
public void Add_輸入負數和正數_應回傳正確結果()
{
// Arrange - 準備測試資料與相依物件
var calculator = new Calculator();
const int a = -5;
const int b = 3;
const int expected = -2;
// Act - 執行被測試的方法
var result = calculator.Add(a, b);
// Assert - 驗證結果是否符合預期
Assert.Equal(expected, result);
}Every test method must follow the Arrange-Act-Assert pattern:
csharp
[Fact]
public void Add_InputNegativeAndPositiveNumbers_ShouldReturnCorrectResult()
{
// Arrange - Prepare test data and dependent objects
var calculator = new Calculator();
const int a = -5;
const int b = 3;
const int expected = -2;
// Act - Execute the method under test
var result = calculator.Add(a, b);
// Assert - Verify if the result meets expectations
Assert.Equal(expected, result);
}各區塊職責
Responsibilities of Each Block
| 區塊 | 職責 | 注意事項 |
|---|---|---|
| Arrange | 準備測試所需的物件、資料、Mock | 使用 |
| Act | 執行被測試的方法 | 通常只有一行,呼叫被測方法 |
| Assert | 驗證結果 | 每個測試只驗證一個行為 |
| Block | Responsibility | Notes |
|---|---|---|
| Arrange | Prepare test data, objects, and Mocks | Use |
| Act | Execute the method under test | Usually only one line, calling the tested method |
| Assert | Verify the result | Each test should only verify one behavior |
測試命名規範
Test Naming Conventions
使用以下格式命名測試方法:
text
[被測試方法名稱]_[測試情境]_[預期行為]Name test methods using the following format:
text
[MethodUnderTest]_[TestScenario]_[ExpectedBehavior]命名範例
Naming Examples
| 方法名稱 | 說明 |
|---|---|
| 測試正常輸入 |
| 測試邊界條件 |
| 測試例外情況 |
| 測試無效輸入 |
| 測試回傳值 |
💡 提示:使用中文命名可以讓測試報告更易讀,特別是在團隊溝通時。
| Method Name | Description |
|---|---|
| Test normal input |
| Test boundary conditions |
| Test exception cases |
| Test invalid input |
| Test return value |
💡 Tip: Using Chinese names for tests makes test reports more readable, especially during team communication.
xUnit 測試屬性
xUnit Test Attributes
[Fact] - 單一測試案例
[Fact] - Single Test Case
用於測試單一情境:
csharp
[Fact]
public void Add_輸入0和0_應回傳0()
{
var calculator = new Calculator();
var result = calculator.Add(0, 0);
Assert.Equal(0, result);
}Used for testing a single scenario:
csharp
[Fact]
public void Add_Input0And0_ShouldReturn0()
{
var calculator = new Calculator();
var result = calculator.Add(0, 0);
Assert.Equal(0, result);
}[Theory] + [InlineData] - 參數化測試
[Theory] + [InlineData] - Parameterized Tests
用於測試多個輸入組合:
csharp
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
[InlineData(100, -50, 50)]
public void Add_輸入各種數值組合_應回傳正確結果(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}Used for testing multiple input combinations:
csharp
[Theory]
[InlineData(1, 2, 3)]
[InlineData(-1, 1, 0)]
[InlineData(0, 0, 0)]
[InlineData(100, -50, 50)]
public void Add_InputVariousNumberCombinations_ShouldReturnCorrectResult(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.Equal(expected, result);
}測試多個無效輸入
Testing Multiple Invalid Inputs
csharp
[Theory]
[InlineData("invalid-email")]
[InlineData("@example.com")]
[InlineData("test@")]
[InlineData("test.example.com")]
public void IsValidEmail_輸入無效Email格式_應回傳False(string invalidEmail)
{
var emailHelper = new EmailHelper();
var result = emailHelper.IsValidEmail(invalidEmail);
Assert.False(result);
}csharp
[Theory]
[InlineData("invalid-email")]
[InlineData("@example.com")]
[InlineData("test@")]
[InlineData("test.example.com")]
public void IsValidEmail_InputInvalidEmailFormats_ShouldReturnFalse(string invalidEmail)
{
var emailHelper = new EmailHelper();
var result = emailHelper.IsValidEmail(invalidEmail);
Assert.False(result);
}例外測試
Exception Testing
測試預期會拋出例外的情況:
csharp
[Fact]
public void Divide_輸入10和0_應拋出DivideByZeroException()
{
// Arrange
var calculator = new Calculator();
const decimal dividend = 10m;
const decimal divisor = 0m;
// Act & Assert
var exception = Assert.Throws<DivideByZeroException>(
() => calculator.Divide(dividend, divisor)
);
// 驗證例外訊息
Assert.Equal("除數不能為零", exception.Message);
}Test scenarios where exceptions are expected:
csharp
[Fact]
public void Divide_Input10And0_ShouldThrowDivideByZeroException()
{
// Arrange
var calculator = new Calculator();
const decimal dividend = 10m;
const decimal divisor = 0m;
// Act & Assert
var exception = Assert.Throws<DivideByZeroException>(
() => calculator.Divide(dividend, divisor)
);
// Verify exception message
Assert.Equal("Divisor cannot be zero", exception.Message);
}測試專案結構
Test Project Structure
建議的專案結構:
text
Solution/
├── src/
│ └── MyProject/
│ ├── Calculator.cs
│ └── MyProject.csproj
└── tests/
└── MyProject.Tests/
├── CalculatorTests.cs
└── MyProject.Tests.csprojRecommended project structure:
text
Solution/
├── src/
│ └── MyProject/
│ ├── Calculator.cs
│ └── MyProject.csproj
└── tests/
└── MyProject.Tests/
├── CalculatorTests.cs
└── MyProject.Tests.csproj測試專案範本 (.csproj)
Test Project Template (.csproj)
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyProject\MyProject.csproj" />
</ItemGroup>
</Project>xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyProject\MyProject.csproj" />
</ItemGroup>
</Project>常用斷言方法
Common Assertion Methods
| 斷言方法 | 用途 |
|---|---|
| 驗證相等 |
| 驗證不相等 |
| 驗證條件為真 |
| 驗證條件為假 |
| 驗證為 null |
| 驗證不為 null |
| 驗證拋出特定例外 |
| 驗證集合為空 |
| 驗證集合包含項目 |
| Assertion Method | Purpose |
|---|---|
| Verify equality |
| Verify inequality |
| Verify condition is true |
| Verify condition is false |
| Verify object is null |
| Verify object is not null |
| Verify specific exception is thrown |
| Verify collection is empty |
| Verify collection contains item |
生成測試的檢查清單
Checklist for Generating Tests
為方法生成測試時,請確保涵蓋:
- 正常路徑 - 標準輸入產生預期輸出
- 邊界條件 - 最小值、最大值、零、空字串
- 無效輸入 - null、負數、格式錯誤
- 例外情況 - 預期會拋出例外的情境
When generating tests for a method, ensure you cover:
- Normal Path - Standard input produces expected output
- Boundary Conditions - Minimum values, maximum values, zero, empty strings
- Invalid Inputs - Null, negative numbers, invalid formats
- Exception Scenarios - Situations where exceptions are expected
參考資源
Reference Resources
原始文章
Original Articles
本技能內容提煉自「老派軟體工程師的測試修練 - 30 天挑戰」系列文章:
- Day 01 - 老派工程師的測試啟蒙
This skill content is extracted from the "Old-School Software Engineer's Testing Practice - 30-Day Challenge" series:
- Day 01 - Testing Enlightenment for Old-School Engineers
- Ironman Challenge Article: https://ithelp.ithome.com.tw/articles/10373888
- Sample Code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day01