TUnit: The New Sheriff in Town for .NET Testing

This blog is presented as a part of C# Advent 2025. Follow the link to check out the rest of the excellent C# and .NET content coming out in 2 blogs per day between December 1 - 25.

For many years, the .NET ecosystem has relied on established testing frameworks such as xUnit.net, NUnit, and MSTest. These frameworks have served teams well for unit, integration, and even acceptance testing. However, as .NET has evolved with newer runtime versions, AOT compilation, cross-platform requirements, increasingly large test suites, and faster CI/CD pipelines the limitations of legacy frameworks have become more apparent.

TUnit aims to address many of these challenges, or at least claims to. In this blog I want to examine those claims and see whether they hold up in practice.

The Problem with Legacy Testing Frameworks

Before discussing TUnit, let’s acknowledge the elephants in the room the PAIN points.

1. Reflection Overhead

xUnit, NUnit, and MSTest rely heavily on reflection to discover tests, instantiate fixtures, and invoke test methods. Reflection is powerful, but it comes at a performance cost. In large test suites, this overhead can compound significantly especially in environments where startup time, test discovery speed, and overall throughput are critical.

2. Async-First Design Was an Afterthought

When async/await became mainstream, legacy frameworks added support rather than redesigning around it. The result is sometimes clunky assertions, inconsistent async handling, and occasional ergonomic friction.

 [Fact]
 public async Task TestAsync()
 {
     var result = await GetDataAsync();
     Assert.Equal(13, result.Count); // No await here, clunky
 }

3. No Native AOT Support

Native AOT is becoming essential for cloud-native applications, edge computing, and containerized workloads. Legacy frameworks heavy reflection usage makes AOT support difficult or impossible.

4. Limited Parallelization Control

Legacy frameworks offer basic parallelization, but fine-grained control over test execution ordering, dependency management, and resource allocation is minimal.

5. Test Discovery Bottlenecks

Reflection-based test discovery happens at runtime, forcing developers to wait before tests even start executing. This is particularly frustrating in CI/CD pipelines.

TUnit Claims

Now that we understand some of the pain points of “legacy frameworks” we can enter the world of TUnit.

TUnit’s design philosophy centers on being fast, modern, flexible, simple, and easy to adopt.

Key concepts:

  • Performance first — using compile-time source generation instead of reflection, with parallel execution enabled by default.
  • Modern .NET alignment — full support for .NET 8 and later, Native AOT, trimming, and new runtime capabilities.
  • Control & flexibility — attribute- and configuration-based workflows, custom data sources, extensible behaviors, dependency chains, and fine-grained scheduling control.
  • Broad applicability — suitable not only for unit tests but also integration, acceptance, and even end-to-end scenarios (including support for libraries like Playwright).

In doing so, TUnit aims to reimagine what testing should look like in modern .NET not just another clunky clone, and certainly not “yet another fork” as many tools tend to become in today’s ecosystem.

Before You Switch to the New and Shiny: A Feature Comparison

Before jumping into a new framework, it’s worth taking a closer look at how TUnit’s features compare to the established players

FeatureTUnitxUnitNUnitMSTest
Source Generated
Native AOT Support
Async-First Assertions
Fluent API
Data SourcesArguments, Method, Class, MatrixInlineData, MemberData, ClassDataTestCase, TestCaseSource, ValuesDataRow, DynamicData
Setup/TeardownBefore/After hooksConstructor/IDisposableSetUp/TearDownTestInitialize/TestCleanup
Attributes on Parameters✅ Matrix✅ Values
Compile-Time Diagnostics⚠️ Minimal⚠️ Minimal⚠️ Minimal
Microsoft Testing Platform✅ (v3+)
Performance vs CompetitionBaseline (1.3x faster than xUnit)1.3x slower1.2x slower1.3x slower

As you can see from the comparison table, TUnit supports many features that other testing frameworks do not. While the speed improvement around 1.3× faster at baseline may not seem dramatic, it is still a measurable gain, especially in large test suites. More importantly, performance is only one part of the story. TUnit introduces several modern capabilities that meaningfully enhance developer experience and test design.

Let’s take a closer look at the features TUnit brings to the table.

Note: For feature comparisons, I will primarily reference xUnit, since it is the most widely used modern framework and has been my personal go-to choice for years.

Native Async Support

TUnit’s most distinctive feature is truly async-first design. Assertions are awaitable, setup hooks are async, and the entire test lifecycle acknowledges that modern .NET is asynchronous.

xUnit approach:

[Fact]
public async Task TestAsync()
{
    var result = await GetDataAsync();
    Assert.NotNull(result);
    Assert.Equal(5, result.Items.Count);
    Assert.True(result.IsValid);
}

Notice the disconnect: we’re inside an async method, yet the assertions are synchronous calls. When an assertion fails, we lose the surrounding async context, which can obscure stack traces and make debugging more difficult.

TUnit approach:

[Test]
public async Task TestAsync()
{
    var result = await GetDataAsync();
    
    await Assert.That(result).IsNotNull();
    await Assert.That(result.Items).Count().IsEqualTo(5);
    await Assert.That(result.IsValid).IsTrue();
}

Each assertion is awaited. This enables TUnit to:

  • Capture the async context accurately
  • Chain assertions fluently
  • Handle async assertions without special or custom syntax
  • Provide clearer, more informative error messages with full execution context

Source Generation and Compile-Time Validation

TUnit uses C# source generators to discover and compile tests at build time, completely eliminating the need for runtime reflection. This approach allows many errors to be caught during compilation rather than at runtime, and it significantly improves test startup time by removing the reflection based discovery phase entirely. It also enhances the overall debugging experience, as the generated code provides clearer execution paths and more predictable behavior compared to traditional reflection driven frameworks.

[Test]
[Arguments(1, 2, 3)]
[Arguments(4, 5, 9)]
public async Task Add_WithVariousInputs(int a, int b, int expected)
{
    var result = Add(a, b);
    await Assert.That(result).IsEqualTo(expected);
}

// Source-generated equivalent (simplified):
// [CompilerGenerated]
// public async Task Add_WithVariousInputs_1()
// {
//     var result = Add(1, 2);
//     await Assert.That(result).IsEqualTo(3);
// }
// [CompilerGenerated]
// public async Task Add_WithVariousInputs_2() { ... }

Parallelization Model

// Run tests in parallel by default
[Test]
[Parallel] // Explicitly parallelizable
public async Task Test1() { }

// Force sequential execution when needed
[Test]
[Sequential] // Don't parallelize this test
public async Task Test2() { }

// Share resources across parallel tests
[ClassDataSource<DatabaseFixture>(Shared = SharedType.PerClass)]
public class DatabaseTests
{
    [Test]
    public async Task Test1(DatabaseFixture db) { }
    
    [Test]
    public async Task Test2(DatabaseFixture db) { }
}

Hence, xUnit can already run tests in parallel, so this is not a new concept. However, let’s give credit where credit is due.

Data-Driven Testing

Compile-time arguments

[Test]
[Arguments(1, 2, 3)]
[Arguments(4, 5, 9)]
public async Task Addition(int a, int b, int expected)
{
    await Assert.That(a + b).IsEqualTo(expected);
}

Method data sources (dynamic data)

[Test]
[MethodDataSource(nameof(TestCases))]
public async Task WithDynamicData(int value, string expected)
{
    var result = ProcessValue(value);
    await Assert.That(result).IsEqualTo(expected);
}

public static IEnumerable<(int, string)> TestCases()
{
    yield return (1, "one");
    yield return (2, "two");
    yield return (3, "three");
}

Matrix tests (combinatorial)

[Test]
public async Task MatrixCombinations(
    [Matrix(1, 2, 3)] int x,
    [Matrix("a", "b")] string y,
    [Matrix(true, false)] bool z)
{
    // Runs 3 × 2 × 2 = 12 combinations automatically
    await Assert.That(Validate(x, y, z)).IsTrue();
}

Class data sources

[ClassDataSource<UserFixture>(Shared = SharedType.PerClass)]
public class UserServiceTests
{
    [Test]
    public async Task CreateUser(UserFixture fixture)
    {
        var user = await fixture.Service.CreateAsync("John");
        await Assert.That(user.Name).IsEqualTo("John");
    }
}

public class UserFixture
{
    public IUserService Service { get; } = new UserService();
}

As you can see, TUnit supports nearly all the features offered by legacy frameworks, which is quite impressive. The creators clearly recognized that most developers rely on these familiar capabilities, so instead of removing them, they refined and modernized them. Features like Matrix Tests, for example, are something I use often yet setting them up in older frameworks was a real pain and definitely contributed to a few gray hairs on my head

Test Lifecycle and Fixtures

public class TestLifecycleExample
{
    private DatabaseConnection _connection;
    
    // Run once per test method
    [Before(Test)]
    public async Task SetupTest()
    {
        _connection = new DatabaseConnection();
        await _connection.OpenAsync();
    }
    
    [Test]
    public async Task DatabaseQuery()
    {
        var result = await _connection.QueryAsync("SELECT * FROM Users");
        await Assert.That(result).IsNotNull();
    }
    
    // Run once per test method (cleanup)
    [After(Test)]
    public async Task CleanupTest()
    {
        await _connection.CloseAsync();
        _connection?.Dispose();
    }
    
    // Run once per test class
    [Before(Class)]
    public static async Task SetupClass()
    {
        // Initialize shared resources
    }
    
    [After(Class)]
    public static async Task CleanupClass()
    {
        // Tear down shared resources
    }
    
    // Run once per assembly
    [Before(Assembly)]
    public static async Task SetupAssembly()
    {
        // Application-wide initialization
    }
}

Built-In Fluent Assertions (No Extra Packages Needed!)

TUnit supports fluent assertion chaining out of the box, one of the features I appreciate most about the framework. The team clearly listened to the community and delivered this capability natively, without requiring any additional packages or setup.

[Test]
public async Task FluentAssertions()
{
    var user = new { Name = "Alice", Age = 30, Email = "alice@example.com" };
    
    // Chain assertions naturally
    await Assert.That(user.Name)
        .IsNotNull()
        .IsNotEmpty()
        .StartsWith("Al");
    
    // Use 'And' for logical grouping
    await Assert.That(user.Age)
        .IsGreaterThan(18)
        .And.IsLessThan(65);
    
    // Multiple assertions in one scope
    using var scope = Assert.Multiple();
    
    await Assert.That(user.Name).IsEqualTo("Alice");
    await Assert.That(user.Age).IsEqualTo(30);
    await Assert.That(user.Email).IsNotNull();
}

Native Dependency Injection Support

You don’t need any convoluted setup to use dependency injection, TUnit supports it out of the box, and that’s incredibly convenient. Having this kind of functionality available without extra configuration is pretty neat.

[Test]
public async Task WithDependencyInjection(IUserService userService, ILogger logger)
{
    var user = await userService.GetUserAsync(1);
    logger.LogInformation($"Loaded user: {user.Name}");
    
    await Assert.That(user).IsNotNull();
}

// Configure via ServiceCollection
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IUserService, UserService>();
    services.AddLogging();
}

Conclusion

Should You Adopt TUnit?

TUnit represents a genuine advancement in .NET testing. This isn’t a marginal quality of life improvement, it’s a re-architecture for the modern .NET era. Legacy frameworks remain solid, reliable, and mature, but they reflect design decisions made long before AOT, source generators, or today’s performance expectations.

For developers starting new projects or optimizing performance-critical test suites, TUnit deserves serious consideration. The best time to adopt TUnit is in greenfield projects. The second best time is when your existing test suite becomes a performance bottleneck or when you need Native AOT support. For established codebases with significant test investment, your existing framework will most likely continue to work just fine, but you may still want to try TUnit and experience the difference firsthand.

Both TUnit and xUnit are powerful testing frameworks, each with strengths that suit different scenarios. Understanding their trade-offs helps you choose the right tool for your project’s requirements.

If you’re looking to modernize your .NET ecosystem or optimize performance across your testing and application layers, the Trailhead team can help. Contact Trailhead today if you’re ready to take your system’s reliability and speed to the next level with better testing.

References & Further Reading

TUnit — Official Website & Documentation
Converting an xUnit Test Project to TUnit — Andrew Lock

Picture of Vladan Petrovic

Vladan Petrovic

Vladan earned his B.S in Computer at Faculty of Organisational Sciences of the University in Belgrade.Vladan is a result-oriented engineer with over 11 years of experience in the development of web and desktop applications. He has a deep knowledge of C#, .NET, and has a strong background in algorithms and data structures, mathematics, statistics, and data analysis. Vladan enjoys tackling complex tasks and is committed to staying up-to-date with the latest technologies and industry trends. Overall, his combination of technical expertise, experience, and passion for software development make him a valuable asset to the team. In his free time, he enjoys traveling to new and interesting places, basketball, photography, blogging, and hiking.

Free Consultation

Sign up for a FREE consultation with one of Trailhead's experts.

"*" indicates required fields

This field is for validation purposes and should be left unchanged.

Related Blog Posts

We hope you’ve found this to be helpful and are walking away with some new, useful insights. If you want to learn more, here are a couple of related articles that others also usually find to be interesting:

Our Gear Is Packed and We're Excited to Explore With You

Ready to come with us? 

Together, we can map your company’s software journey and start down the right trails. If you’re set to take the first step, simply fill out our contact form. We’ll be in touch quickly – and you’ll have a partner who is ready to help your company take the next step on its software journey. 

We can’t wait to hear from you! 

Main Contact

This field is for validation purposes and should be left unchanged.

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the form below. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Montage Portal

Montage Furniture Services provides furniture protection plans and claims processing services to a wide selection of furniture retailers and consumers.

Project Background

Montage was looking to build a new web portal for both Retailers and Consumers, which would integrate with Dynamics CRM and other legacy systems. The portal needed to be multi tenant and support branding and configuration for different Retailers. Trailhead architected the new Montage Platform, including the Portal and all of it’s back end integrations, did the UI/UX and then delivered the new system, along with enhancements to DevOps and processes.

Logistics

We’ve logged countless miles exploring the tech world. In doing so, we gained the experience that enables us to deliver your unique software and systems architecture needs. Our team of seasoned tech vets can provide you with:

Custom App and Software Development

We collaborate with you throughout the entire process because your customized tech should fit your needs, not just those of other clients.

Cloud and Mobile Applications

The modern world demands versatile technology, and this is exactly what your mobile and cloud-based apps will give you.

User Experience and Interface (UX/UI) Design

We want your end users to have optimal experiences with tech that is highly intuitive and responsive.

DevOps

This combination of Agile software development and IT operations provides you with high-quality software at reduced cost, time, and risk.

Trailhead stepped into a challenging project – building our new web architecture and redeveloping our portals at the same time the business was migrating from a legacy system to our new CRM solution. They were able to not only significantly improve our web development architecture but our development and deployment processes as well as the functionality and performance of our portals. The feedback from customers has been overwhelmingly positive. Trailhead has proven themselves to be a valuable partner.

– BOB DOERKSEN, Vice President of Technology Services
at Montage Furniture Services

Technologies Used

When you hit the trails, it is essential to bring appropriate gear. The same holds true for your digital technology needs. That’s why Trailhead builds custom solutions on trusted platforms like .NET, Angular, React, and Xamarin.

Expertise

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

  • Project Management
  • Architecture
  • Web App Development
  • Cloud Development
  • DevOps
  • Process Improvements
  • Legacy System Integration
  • UI Design
  • Manual QA
  • Back end/API/Database development

We partner with businesses who need intuitive custom software, responsive mobile applications, and advanced cloud technologies. And our extensive experience in the tech field allows us to help you map out the right path for all your digital technology needs.

Our Gear Is Packed and We're Excited to Explore with You

Ready to come with us? 

Together, we can map your company’s tech journey and start down the trails. If you’re set to take the first step, simply fill out the contact form. We’ll be in touch – and you’ll have a partner who cares about you and your company. 

We can’t wait to hear from you! 

Thank you for reaching out.

You’ll be getting an email from our team shortly. If you need immediate assistance, please call (616) 371-1037.