SuperAPI AI Directory

dotnet-testing-unit-test-fundamentals

Testing & quality

View repository

Updated 10-15 23:45

About this skill

.NET Unit Testing Fundamentals Guide

FIRST Principles

Good unit tests follow the principles below, because these principles ensure the reliability and maintainability of tests:

F - Fast

Test execution time should be in the millisecond range and should not depend on external resources.

[Fact] // Fast: 不依賴外部資源,執行快速
public void Add_輸入1和2_應回傳3()
{
    // 純記憶體運算,無 I/O 或網路延遲
    var calculator = new Calculator();
    var result = calculator.Add(1, 2);
    Assert.Equal(3, result);
}

I - Independent

There should be no dependencies between tests; each test creates a new instance.

[Fact] // Independent: 每個測試都建立新的實例
public void Increment_從0開始_應回傳1()
{
    var counter = new Counter(); // 每個測試都建立新的實例,不受其他測試影響
    counter.Increment();
    Assert.Equal(1, counter.Value);
}

R - Repeatable

The same result can be obtained in any environment, without depending on external state.

[Fact] // Repeatable: 每次執行都得到相同結果
public void Increment_多次執行_應產生一致結果()
{
    var counter = new Counter();
    counter.Increment();
    counter.Increment();
    counter.Increment();
    
    // 每次執行這個測試都會得到相同結果
    Assert.Equal(3, counter.Value);
}

S - Self-Validating

The test result should be a clear pass or fail, using clear assertions.

[Fact] // Self-Validating: 明確的驗證
public void IsValidEmail_輸入有效Email_應回傳True()
{
    var emailHelper = new EmailHelper();
    var result = emailHelper.IsValidEmail("test@example.com");
    
    Assert.True(result); // 明確的通過或失敗
}

T - Timely

Tests should be written before or at the same time as the production code, ensuring the testability of the code.

3A Pattern Structure

Each test method follows the Arrange-Act-Assert pattern; this structure makes the intent of the test clear at a glance:

[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);
}

Responsibilities of Each Block

Block Responsibility Notes
Arrange Prepare the objects, data, and Mocks needed for the test Use const to declare constant values and improve readability
Act Execute the method under test Usually only one line, calling the method under test
Assert Verify the result Each test verifies only one behavior

Test Naming Conventions

Name test methods using the following format:

[被測試方法名稱]_[測試情境]_[預期行為]

Naming Examples

Method Name Description
Add_輸入1和2_應回傳3 Test normal input
Add_輸入負數和正數_應回傳正確結果 Test boundary conditions
Divide_輸入10和0_應拋出DivideByZeroException Test exception cases
IsValidEmail_輸入null值_應回傳False Test invalid input
GetDomain_輸入有效Email_應回傳網域名稱 Test return values

Tip: Using Chinese names can make test reports easier to read, especially during team communication.

xUnit Test Attributes

[Fact] - Single Test Case

Used to test a single scenario:

[Fact]
public void Add_輸入0和0_應回傳0()
{
    var calculator = new Calculator();
    var result = calculator.Add(0, 0);
    Assert.Equal(0, result);
}

[Theory] + [InlineData] - Parameterized Tests

Used to test multiple input combinations:

[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);
}

Test multiple invalid inputs

[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);
}

Exception Tests

Test cases where an exception is expected to be thrown:

[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 Project Structure

Recommended project structure:

Solution/
├── src/
│   └── MyProject/
│       ├── Calculator.cs
│       └── MyProject.csproj
└── tests/
    └── MyProject.Tests/
        ├── CalculatorTests.cs
        └── MyProject.Tests.csproj

Test Project Template (.csproj)

<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="8.0.1">
            <PrivateAssets>all</PrivateAssets>
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
        <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

Assertion Method Purpose
Assert.Equal(expected, actual) Verify equality
Assert.NotEqual(expected, actual) Verify inequality
Assert.True(condition) Verify that a condition is true
Assert.False(condition) Verify that a condition is false
Assert.Null(object) Verify that it is null
Assert.NotNull(object) Verify that it is not null
Assert.Throws<T>(action) Verify that a specific exception is thrown
Assert.Empty(collection) Verify that a collection is empty
Assert.Contains(item, collection) Verify that a collection contains an item

Checklist for Generating Tests

When generating tests for a method, make sure to cover:

  • Normal path - Standard input produces the expected output
  • Boundary conditions - minimum, maximum, zero, empty string
  • Invalid input - null, negative numbers, malformed
  • Exception cases - scenarios where an exception is expected to be thrown

Output format

  • Generate an xUnit test class (.cs file) that complies with the FIRST principles
  • Each test method follows the AAA Pattern (Arrange-Act-Assert)
  • Use three-part naming (Method_Scenario_Expected)
  • Cover the happy path, boundary conditions, invalid input, and exception cases

Reference resources

Original article

The content of this skill is distilled from the "Old-School Software Engineer's Testing Practice - 30-Day Challenge" article series:

Related skills

  • dotnet-testing-test-naming-conventions - Test naming conventions
  • dotnet-testing-xunit-project-setup - xUnit project setup
  • dotnet-testing-awesome-assertions-guide - Fluent assertions