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使用
const
宣告常數值,提高可讀性
Act執行被測試的方法通常只有一行,呼叫被測方法
Assert驗證結果每個測試只驗證一個行為
BlockResponsibilityNotes
ArrangePrepare test data, objects, and MocksUse
const
for constant values to improve readability
ActExecute the method under testUsually only one line, calling the tested method
AssertVerify the resultEach test should only verify one behavior

測試命名規範

Test Naming Conventions

使用以下格式命名測試方法:
text
[被測試方法名稱]_[測試情境]_[預期行為]
Name test methods using the following format:
text
[MethodUnderTest]_[TestScenario]_[ExpectedBehavior]

命名範例

Naming Examples

方法名稱說明
Add_輸入1和2_應回傳3
測試正常輸入
Add_輸入負數和正數_應回傳正確結果
測試邊界條件
Divide_輸入10和0_應拋出DivideByZeroException
測試例外情況
IsValidEmail_輸入null值_應回傳False
測試無效輸入
GetDomain_輸入有效Email_應回傳網域名稱
測試回傳值
💡 提示:使用中文命名可以讓測試報告更易讀,特別是在團隊溝通時。
Method NameDescription
Add_Input1And2_ShouldReturn3
Test normal input
Add_InputNegativeAndPositiveNumbers_ShouldReturnCorrectResult
Test boundary conditions
Divide_Input10And0_ShouldThrowDivideByZeroException
Test exception cases
IsValidEmail_InputNullValue_ShouldReturnFalse
Test invalid input
GetDomain_InputValidEmail_ShouldReturnDomainName
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.csproj
Recommended 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

斷言方法用途
Assert.Equal(expected, actual)
驗證相等
Assert.NotEqual(expected, actual)
驗證不相等
Assert.True(condition)
驗證條件為真
Assert.False(condition)
驗證條件為假
Assert.Null(object)
驗證為 null
Assert.NotNull(object)
驗證不為 null
Assert.Throws<T>(action)
驗證拋出特定例外
Assert.Empty(collection)
驗證集合為空
Assert.Contains(item, collection)
驗證集合包含項目
Assertion MethodPurpose
Assert.Equal(expected, actual)
Verify equality
Assert.NotEqual(expected, actual)
Verify inequality
Assert.True(condition)
Verify condition is true
Assert.False(condition)
Verify condition is false
Assert.Null(object)
Verify object is null
Assert.NotNull(object)
Verify object is not null
Assert.Throws<T>(action)
Verify specific exception is thrown
Assert.Empty(collection)
Verify collection is empty
Assert.Contains(item, collection)
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 天挑戰」系列文章:
This skill content is extracted from the "Old-School Software Engineer's Testing Practice - 30-Day Challenge" series: