SuperAPI AI Directory

dotnet-testing-advanced-tunit-advanced

Testing & quality

View repository

Updated 10-15 23:45

About this skill

Advanced TUnit Applications: Data-Driven Testing, Dependency Injection, and Integration Testing in Practice

Advanced Techniques for Data-Driven Testing

TUnit provides three advanced data sources: MethodDataSource, ClassDataSource, and Matrix Tests. MethodDataSource is the most flexible, supporting dynamic generation and loading from external files; ClassDataSource is suitable for sharing data across test classes and integrating with AutoFixture; Matrix Tests automatically generates all parameter combinations (be careful to control the number to avoid explosive growth).

For complete examples and a comparison table, see references/data-driven-testing.md


Properties Attribute Marking and Test Filtering

Basic Properties Usage

[Test]
[Property("Category", "Database")]
[Property("Priority", "High")]
public async Task DatabaseTest_高優先級_應能透過屬性過濾()
{
    await Assert.That(true).IsTrue();
}

[Test]
[Property("Category", "Unit")]
[Property("Priority", "Medium")]
public async Task UnitTest_中等優先級_基本驗證()
{
    await Assert.That(1 + 1).IsEqualTo(2);
}

[Test]
[Property("Category", "Integration")]
[Property("Priority", "Low")]
[Property("Environment", "Development")]
public async Task IntegrationTest_低優先級_僅開發環境執行()
{
    await Assert.That("Hello World").Contains("World");
}

Establish a Consistent Property Naming Convention

public static class TestProperties
{
    // 測試類別
    public const string CATEGORY_UNIT = "Unit";
    public const string CATEGORY_INTEGRATION = "Integration";
    public const string CATEGORY_E2E = "E2E";

    // 優先級
    public const string PRIORITY_CRITICAL = "Critical";
    public const string PRIORITY_HIGH = "High";
    public const string PRIORITY_MEDIUM = "Medium";
    public const string PRIORITY_LOW = "Low";

    // 環境
    public const string ENV_DEVELOPMENT = "Development";
    public const string ENV_STAGING = "Staging";
    public const string ENV_PRODUCTION = "Production";
}

[Test]
[Property("Category", TestProperties.CATEGORY_UNIT)]
[Property("Priority", TestProperties.PRIORITY_HIGH)]
public async Task ExampleTest_使用常數_確保一致性()
{
    await Assert.That(1 + 1).IsEqualTo(2);
}

TUnit Test Filter Execution

TUnit uses dotnet run instead of dotnet test:

# 只執行單元測試
dotnet run --treenode-filter "/*/*/*/*[Category=Unit]"

# 只執行高優先級測試
dotnet run --treenode-filter "/*/*/*/*[Priority=High]"

# 組合條件:執行高優先級的單元測試
dotnet run --treenode-filter "/*/*/*/*[(Category=Unit)&(Priority=High)]"

# 或條件:執行單元測試或冒煙測試
dotnet run --treenode-filter "/*/*/*/*[(Category=Unit)|(Suite=Smoke)]"

# 執行特定功能的測試
dotnet run --treenode-filter "/*/*/*/*[Feature=OrderProcessing]"

Filter syntax notes:

  • Path pattern /*/*/*/* represents the Assembly/Namespace/Class/Method levels
  • Property names are case-sensitive
  • Combined conditions must be correctly enclosed in parentheses

Test Lifecycle Management

TUnit provides a complete set of lifecycle hooks:[Before(Class)] → constructor → [Before(Test)] → test method → [After(Test)] → Dispose → [After(Class)]. There are also Assembly/TestSession-level and [BeforeEvery]/[AfterEvery] global hooks. The constructor always runs first, and BeforeClass/AfterClass each run only once.

For the complete attribute family and examples, see references/lifecycle-management.md


Dependency Injection Patterns

Core Concepts of TUnit Dependency Injection

TUnit's dependency injection is built on Data Source Generators:

public class MicrosoftDependencyInjectionDataSourceAttribute : DependencyInjectionDataSourceAttribute<IServiceScope>
{
    private static readonly IServiceProvider ServiceProvider = CreateSharedServiceProvider();

    public override IServiceScope CreateScope(DataGeneratorMetadata dataGeneratorMetadata)
    {
        return ServiceProvider.CreateScope();
    }

    public override object? Create(IServiceScope scope, Type type)
    {
        return scope.ServiceProvider.GetService(type);
    }

    private static IServiceProvider CreateSharedServiceProvider()
    {
        return new ServiceCollection()
            .AddSingleton<IOrderRepository, MockOrderRepository>()
            .AddSingleton<IDiscountCalculator, MockDiscountCalculator>()
            .AddSingleton<IShippingCalculator, MockShippingCalculator>()
            .AddSingleton<ILogger<OrderService>, MockLogger<OrderService>>()
            .AddTransient<OrderService>()
            .BuildServiceProvider();
    }
}

Using TUnit Dependency Injection

[MicrosoftDependencyInjectionDataSource]
public class DependencyInjectionTests(OrderService orderService)
{
    [Test]
    public async Task CreateOrder_使用TUnit依賴注入_應正確運作()
    {
        // Arrange - 依賴已經透過 TUnit DI 自動注入
        var items = new List<OrderItem>
        {
            new() { ProductId = "PROD001", ProductName = "測試商品", UnitPrice = 100m, Quantity = 2 }
        };

        // Act
        var order = await orderService.CreateOrderAsync("CUST001", CustomerLevel.VIP會員, items);

        // Assert
        await Assert.That(order).IsNotNull();
        await Assert.That(order.CustomerId).IsEqualTo("CUST001");
        await Assert.That(order.CustomerLevel).IsEqualTo(CustomerLevel.VIP會員);
    }

    [Test]
    public async Task TUnitDependencyInjection_驗證自動注入_服務應為正確類型()
    {
        await Assert.That(orderService).IsNotNull();
        await Assert.That(orderService.GetType().Name).IsEqualTo("OrderService");
    }
}

Comparison of TUnit DI vs Manual Dependency Creation

Aspect TUnit DI Manual Dependency Creation
Setup Complexity Set up once, reuse repeatedly Must be created manually for each test
Maintainability Dependency changes require modifying only one place All tests that use it need to be modified
Consistency Consistent with the DI in production code May be inconsistent with the actual application
Test Readability Focuses on test logic Interfered with by dependency creation code
Scope Management Automatically manages service scopes Requires manual management of object lifecycles
Risk of Errors The framework guarantees correct dependency injection Some dependencies may be missed or created incorrectly

Execution Control and Test Quality

  • [Retry(n)]: Use only for unstable tests caused by external dependencies (network, file locking), not for logic errors
  • [Timeout(ms)]: Set a reasonable upper limit for performance-sensitive tests, paired with Stopwatch to verify the SLA
  • [DisplayName]: Supports {0} parameter interpolation, making test reports closer to business language

For complete examples (Retry/Timeout/DisplayName), see references/execution-control.md


ASP.NET Core Integration Testing

Using in TUnit WebApplicationFactory<Program> Perform ASP.NET Core integration testing by implementing IDisposable to manage the lifecycle. Covers API response validation, Content-Type header checks, as well as performance benchmarks and concurrent load testing.

For a complete WebApplicationFactory integration and load testing example, see references/aspnet-integration.md


TUnit + Testcontainers Infrastructure Orchestration

Use [Before(Assembly)] / [After(Assembly)] to manage multi-container orchestration such as PostgreSQL, Redis, Kafka, etc. at the Assembly level, together with NetworkBuilder to establish a shared network. Containers are started only once, greatly reducing startup time and resource consumption while maintaining data isolation between tests.

For a complete multi-container orchestration and global sharing example, see references/tunit-testcontainers.md


TUnit Engine Modes

Source Generation Mode (Default Mode)

████████╗██╗   ██╗███╗   ██╗██╗████████╗
╚══██╔══╝██║   ██║████╗  ██║██║╚══██╔══╝
   ██║   ██║   ██║██╔██╗ ██║██║   ██║
   ██║   ██║   ██║██║╚██╗██║██║   ██║
   ██║   ╚██████╔╝██║ ╚████║██║   ██║
   ╚═╝    ╚═════╝ ╚═╝  ╚═══╝╚═╝   ╚═╝

   Engine Mode: SourceGenerated

Features and Advantages:

  • Compile-time generation: All test discovery logic is generated at compile time, no runtime reflection required
  • Excellent performance: Several times faster than reflection mode
  • Type safety: Compile-time validation of test configuration and data sources
  • AOT compatible: Full support for Native AOT compilation

Reflection Mode

# 啟用反射模式
dotnet run -- --reflection

# 或設定環境變數
$env:TUNIT_EXECUTION_MODE = "reflection"
dotnet run

Applicable scenarios:

  • Dynamic test discovery
  • F# and VB.NET projects (used automatically)
  • Certain test patterns that rely on reflection

Native AOT Support

<PropertyGroup>
    <PublishAot>true</PublishAot>
</PropertyGroup>
dotnet publish -c Release

Common Issues and Troubleshooting

Abnormal test statistics issue

Symptoms: 測試摘要: 總計: 0, 失敗: 0, 成功: 0

Resolution steps:

  1. Ensure the project file is configured correctly:
<PropertyGroup>
    <IsTestProject>true</IsTestProject>
</PropertyGroup>
  1. Ensure GlobalUsings.cs is correct:
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
global using TUnit.Core;
global using TUnit.Assertions;
global using TUnit.Assertions.Extensions;
  1. Special configuration for integration tests:
// 在 WebApi 專案的 Program.cs 最後加上
public partial class Program { }  // 讓整合測試可以存取
  1. Clean and rebuild:
dotnet clean; dotnet build
dotnet test --verbosity normal

Source Generator Related Issues

Issue: Test classes cannot be discovered

  • Solution: Ensure the project is fully rebuilt (dotnet clean; dotnet build)

Issue: Strange errors during compilation

  • Solution: Check whether there are other Source Generator packages, and consider updating to compatible versions

Diagnostic Options

# .editorconfig
tunit.enable_verbose_diagnostics = true
<PropertyGroup>
    <TUnitEnableVerboseDiagnostics>true</TUnitEnableVerboseDiagnostics>
</PropertyGroup>

Practical Recommendations

Selection Strategy for Data-Driven Testing

  • MethodDataSource: Suitable for dynamic data, complex objects, and external file loading
  • ClassDataSource: Suitable for shared data, AutoFixture integration, and reuse across test classes
  • Matrix Tests: Suitable for combinatorial testing, but be mindful of the number of parameters to avoid explosive growth

Execution Control Best Practices

  • Retry: Use only for genuinely unstable external dependency tests
  • Timeout: Set reasonable limits for performance-sensitive tests
  • DisplayName: Make test reports more aligned with business language

Integration Testing Strategy

  • Use WebApplicationFactory for complete Web API testing
  • Use TUnit + Testcontainers to build complex multi-service test environments
  • Manage complex dependencies through the property injection system
  • Only test functionality that actually exists, and avoid testing endpoints that do not exist

Template Files

File Name Description
data-source-examples.cs MethodDataSource, ClassDataSource examples
matrix-tests-examples.cs Matrix Tests combinatorial testing examples
lifecycle-di-examples.cs Lifecycle management and dependency injection examples
execution-control-examples.cs Retry, Timeout, DisplayName examples
aspnet-integration-tests.cs ASP.NET Core Integration Testing Example
testcontainers-examples.cs Testcontainers Infrastructure Orchestration Example

Output Format

  • Generate data-driven test classes (.cs files) using MethodDataSource/ClassDataSource/Matrix Tests
  • Includes MicrosoftDependencyInjectionDataSource dependency injection configuration
  • Includes Retry/Timeout/DisplayName execution control examples
  • Generate WebApplicationFactory integration tests and Testcontainers multi-container orchestration code

Reference Resources

Original Articles

This skill content is distilled from the "Old-School Software Engineer's Testing Training - 30-Day Challenge" series of articles:

TUnit Official Resources

Advanced Feature Documentation

Testcontainers Related Resources

Related Skills

  • dotnet-testing-advanced-tunit-fundamentals - TUnit Fundamentals (Prerequisite Skill)
  • dotnet-testing-advanced-aspnet-integration-testing - ASP.NET Core Integration Testing
  • dotnet-testing-advanced-testcontainers-database - Testcontainers Database Testing