
🚀 The Modern Testing Framework for .NET
TUnit is a modern testing framework for .NET that uses source-generated tests, parallel execution by default, and Native AOT support. Built on Microsoft.Testing.Platform, it's faster than traditional reflection-based frameworks and gives you more control over how your tests run.
Why TUnit?
| Feature | Traditional Frameworks | TUnit |
|---|---|---|
| Test Discovery | ❌ Runtime reflection | ✅ Compile-time generation |
| Execution Speed | ❌ Sequential by default | ✅ Parallel by default |
| Modern .NET | ⚠️ Limited AOT support | ✅ Native AOT & trimming |
| Test Dependencies | ❌ Not supported | ✅ [DependsOn] chains |
| Resource Management | ❌ Manual lifecycle | ✅ Automatic cleanup |
Parallel by Default - Tests run concurrently with dependency management
Compile-Time Discovery - Test structure is known before runtime
Modern .NET Ready - Native AOT, trimming, and latest .NET features
Extensible - Customize data sources, attributes, and test behavior
Documentation
New to TUnit? Start with the Getting Started Guide
Migrating? See the Migration Guides
Learn more: Data-Driven Testing, Test Dependencies, Parallelism Control
Quick Start
Using the Project Template (Recommended)
dotnet new install TUnit.Templates
dotnet new TUnit -n "MyTestProject"
Manual Installation
dotnet add package TUnit --prerelease
📖 Complete Documentation & Guides
Key Features
|
Performance
|
Test Control
|
|
Data & Assertions
|
Developer Tools
|
Simple Test Example
[Test]
public async Task User_Creation_Should_Set_Timestamp()
{
// Arrange
var userService = new UserService();
// Act
var user = await userService.CreateUserAsync("john.doe@example.com");
// Assert - TUnit's fluent assertions
await Assert.That(user.CreatedAt)
.IsEqualTo(DateTime.Now)
.Within(TimeSpan.FromMinutes(1));
await Assert.That(user.Email)
.IsEqualTo("john.doe@example.com");
}
Data-Driven Testing
[Test]
[Arguments("user1@test.com", "ValidPassword123")]
[Arguments("user2@test.com", "AnotherPassword456")]
[Arguments("admin@test.com", "AdminPass789")]
public async Task User_Login_Should_Succeed(string email, string password)
{
var result = await authService.LoginAsync(email, password);
await Assert.That(result.IsSuccess).IsTrue();
}
// Matrix testing - tests all combinations
[Test]
[MatrixDataSource]
public async Task Database_Operations_Work(
[Matrix("Create", "Update", "Delete")] string operation,
[Matrix("User", "Product", "Order")] string entity)
{
await Assert.That(await ExecuteOperation(operation, entity))
.IsTrue();
}
Advanced Test Orchestration
[Before(Class)]
public static async Task SetupDatabase(ClassHookContext context)
{
await DatabaseHelper.InitializeAsync();
}
[Test, DisplayName("Register a new account")]
[MethodDataSource(nameof(GetTestUsers))]
public async Task Register_User(string username, string password)
{
// Test implementation
}
[Test, DependsOn(nameof(Register_User))]
[Retry(3)] // Retry on failure
public async Task Login_With_Registered_User(string username, string password)
{
// This test runs after Register_User completes
}
[Test]
[ParallelLimit<LoadTestParallelLimit>] // Custom parallel control
[Repeat(100)] // Run 100 times
public async Task Load_Test_Homepage()
{
// Performance testing
}
// Custom attributes
[Test, WindowsOnly, RetryOnHttpError(5)]
public async Task Windows_Specific_Feature()
{
// Platform-specific test with custom retry logic
}
public class LoadTestParallelLimit : IParallelLimit
{
public int Limit => 10; // Limit to 10 concurrent executions
}
Custom Test Control
// Custom conditional execution
public class WindowsOnlyAttribute : SkipAttribute
{
public WindowsOnlyAttribute() : base("Windows only test") { }
public override Task<bool> ShouldSkip(TestContext testContext)
=> Task.FromResult(!OperatingSystem.IsWindows());
}
// Custom retry logic
public class RetryOnHttpErrorAttribute : RetryAttribute
{
public RetryOnHttpErrorAttribute(int times) : base(times) { }
public override Task<bool> ShouldRetry(TestInformation testInformation,
Exception exception, int currentRetryCount)
=> Task.FromResult(exception is HttpRequestException { StatusCode: HttpStatusCode.ServiceUnavailable });
}
Common Use Cases
Unit Testing
|
Integration Testing
|
Load Testing
|
What Makes TUnit Different?
Compile-Time Test Discovery
Tests are discovered at build time, not runtime. This means faster discovery, better IDE integration, and more predictable resource management.
Parallel by Default
Tests run in parallel by default. Use [DependsOn] to chain tests together, and [ParallelLimit] to control resource usage.
Extensible
The DataSourceGenerator<T> pattern and custom attribute system let you extend TUnit without modifying the framework.
Community & Ecosystem
Resources
- Official Documentation - Guides, tutorials, and API reference
- GitHub Discussions - Get help and share ideas
- Issue Tracking - Report bugs and request features
- Release Notes - Latest updates and changes
IDE Support
TUnit works with all major .NET IDEs:
Visual Studio (2022 17.13+)
✅ Fully supported - No additional configuration needed for latest versions
⚙️ Earlier versions: Enable "Use testing platform server mode" in Tools > Manage Preview Features
JetBrains Rider
✅ Fully supported
⚙️ Setup: Enable "Testing Platform support" in Settings > Build, Execution, Deployment > Unit Testing > Testing Platform
Visual Studio Code
✅ Fully supported
⚙️ Setup: Install C# Dev Kit and enable "Use Testing Platform Protocol"
Command Line
✅ Full CLI support - Works with dotnet test, dotnet run, and direct executable execution
Package Options
| Package | Use Case |
|---|---|
TUnit |
Start here - Complete testing framework (includes Core + Engine + Assertions) |
TUnit.Core |
Test libraries and shared components (no execution engine) |
TUnit.Engine |
Test execution engine and adapter (for test projects) |
TUnit.Assertions |
Standalone assertions (works with any test framework) |
TUnit.Playwright |
Playwright integration with automatic lifecycle management |
Migration from Other Frameworks
Coming from NUnit or xUnit? TUnit uses familiar syntax with some additions:
// TUnit test with dependency management and retries
[Test]
[Arguments("value1")]
[Arguments("value2")]
[Retry(3)]
[ParallelLimit<CustomLimit>]
public async Task Modern_TUnit_Test(string value) { }
📖 Need help migrating? Check our Migration Guides for xUnit, NUnit, and MSTest.
Getting Started
# Create a new test project
dotnet new install TUnit.Templates && dotnet new TUnit -n "MyTestProject"
# Or add to existing project
dotnet add package TUnit --prerelease
Learn More: tunit.dev | Get Help: GitHub Discussions | Star on GitHub: github.com/thomhurst/TUnit
Performance Benchmark
Scenario: Building the test project
BenchmarkDotNet v0.15.6, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
AMD EPYC 7763 2.60GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.100-rc.2.25502.107
[Host] : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Job-GVKUBM : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Runtime=.NET 10.0
| Method | Version | Mean | Error | StdDev | Median |
|---|---|---|---|---|---|
| Build_TUnit | 1.0.0 | 1.788 s | 0.0354 s | 0.0348 s | 1.773 s |
| Build_NUnit | 4.4.0 | 1.621 s | 0.0200 s | 0.0187 s | 1.617 s |
| Build_MSTest | 4.0.1 | 1.667 s | 0.0294 s | 0.0275 s | 1.657 s |
| Build_xUnit3 | 3.2.0 | 1.580 s | 0.0149 s | 0.0133 s | 1.579 s |
Scenario: Tests running asynchronous operations and async/await patterns
BenchmarkDotNet v0.15.6, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.100-rc.2.25502.107
[Host] : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Job-GVKUBM : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Runtime=.NET 10.0
| Method | Version | Mean | Error | StdDev | Median |
|---|---|---|---|---|---|
| TUnit | 1.0.0 | 547.6 ms | 2.37 ms | 2.10 ms | 547.0 ms |
| NUnit | 4.4.0 | 664.4 ms | 6.93 ms | 6.14 ms | 664.5 ms |
| MSTest | 4.0.1 | 636.8 ms | 5.68 ms | 4.74 ms | 636.2 ms |
| xUnit3 | 3.2.0 | 728.5 ms | 6.82 ms | 6.38 ms | 727.8 ms |
| TUnit_AOT | 1.0.0 | 124.2 ms | 0.38 ms | 0.36 ms | 124.2 ms |
Scenario: Parameterized tests with multiple test cases using data attributes
BenchmarkDotNet v0.15.6, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.100-rc.2.25502.107
[Host] : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Job-GVKUBM : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Runtime=.NET 10.0
| Method | Version | Mean | Error | StdDev | Median |
|---|---|---|---|---|---|
| TUnit | 1.0.0 | 482.67 ms | 5.839 ms | 5.176 ms | 481.29 ms |
| NUnit | 4.4.0 | 681.34 ms | 7.060 ms | 5.896 ms | 682.69 ms |
| MSTest | 4.0.1 | 684.85 ms | 8.469 ms | 7.508 ms | 683.64 ms |
| xUnit3 | 3.2.0 | 692.64 ms | 11.163 ms | 9.896 ms | 691.92 ms |
| TUnit_AOT | 1.0.0 | 24.90 ms | 0.164 ms | 0.128 ms | 24.92 ms |
Scenario: Tests executing massively parallel workloads with CPU-bound, I/O-bound, and mixed operations
BenchmarkDotNet v0.15.6, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.100-rc.2.25502.107
[Host] : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Job-GVKUBM : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Runtime=.NET 10.0
| Method | Version | Mean | Error | StdDev | Median |
|---|---|---|---|---|---|
| TUnit | 1.0.0 | 570.2 ms | 7.34 ms | 6.51 ms | 569.1 ms |
| NUnit | 4.4.0 | 1,215.5 ms | 9.85 ms | 8.22 ms | 1,217.3 ms |
| MSTest | 4.0.1 | 2,986.8 ms | 8.18 ms | 7.25 ms | 2,986.6 ms |
| xUnit3 | 3.2.0 | 3,083.5 ms | 8.80 ms | 7.80 ms | 3,084.8 ms |
| TUnit_AOT | 1.0.0 | 130.7 ms | 0.35 ms | 0.33 ms | 130.8 ms |
Scenario: Tests with complex parameter combinations creating 25-125 test variations
BenchmarkDotNet v0.15.6, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
AMD EPYC 7763 2.45GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.100-rc.2.25502.107
[Host] : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Job-GVKUBM : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v3
Runtime=.NET 10.0
| Method | Version | Mean | Error | StdDev | Median |
|---|---|---|---|---|---|
| TUnit | 1.0.0 | 543.36 ms | 3.349 ms | 2.969 ms | 543.07 ms |
| NUnit | 4.4.0 | 1,546.26 ms | 11.102 ms | 10.385 ms | 1,543.32 ms |
| MSTest | 4.0.1 | 1,506.79 ms | 12.194 ms | 11.406 ms | 1,505.80 ms |
| xUnit3 | 3.2.0 | 1,595.43 ms | 8.727 ms | 8.163 ms | 1,592.78 ms |
| TUnit_AOT | 1.0.0 | 78.98 ms | 0.194 ms | 0.172 ms | 78.96 ms |
Scenario: Large-scale parameterized tests with 100+ test cases testing framework scalability
BenchmarkDotNet v0.15.6, Linux Ubuntu 24.04.3 LTS (Noble Numbat)
Intel Xeon Platinum 8370C CPU 2.80GHz, 1 CPU, 4 logical and 2 physical cores
.NET SDK 10.0.100-rc.2.25502.107
[Host] : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v4
Job-GVKUBM : .NET 10.0.0 (10.0.0-rc.2.25502.107, 10.0.25.50307), X64 RyuJIT x86-64-v4
Runtime=.NET 10.0
| Method | Version | Mean | Error | StdDev | Median |
|---|---|---|---|---|---|
| TUnit | 1.0.0 | 479.85 ms | 8.441 ms | 7.896 ms | 480.11 ms |
| NUnit | 4.4.0 | 674.20 ms | 13.299 ms | 16.332 ms | 674.47 ms |
| MSTest | 4.0.1 | 660.43 ms | 9.629 ms | 8.536 ms | 660.28 ms |
| xUnit3 | 3.2.0 | 672.82 ms | 13.455 ms | 15.495 ms | 669.72 ms |
| TUnit_AOT | 1.0.0 | 44.38 ms | 1.352 ms | 3.987 ms | 44.67 ms |