The .NET testing ecosystem has been dominated for years by well‑established frameworks such as xUnit, NUnit, and MSTest. They are mature, stable, and battle‑tested. But as .NET itself evolves, becoming faster, more async‑friendly, and more careful about performance, new tools are emerging to better match these modern expectations.

One of the most interesting newcomers is TUnit .

In this article, we’ll try to explore what TUnit is, its core concepts and advantages, but also explore concrete examples of tests compared to xUnit.
Let’s go ! 🔥


What is TUnit?

TUnit is a modern testing framework for .NET designed with today’s runtime capabilities in mind. It relies on:

  • Async‑first APIs & assertions
  • Parallel execution by default
  • Native AOT, which offers great performance against other JIT frameworks
  • Source generators instead of heavy reflection, which is one of the real game changer.
  • A minimal and explicit programming model

Rather than incrementally extending old abstractions, TUnit rethinks what a test framework should look like in the era of .NET 10, with async/await everywhere, and high‑performance tooling.


Core design principles of TUnit

1. Async‑First by Design

In TUnit, asynchronous tests are not a special case, they are the default. There is no conceptual split between sync and async tests.

[Test]
public async Task GetDataAsync_NominalCase()
{
    // Act
    var result = await GetDataAsync();
    
    // Assert
    await Assert.That(result).IsNotNull();
    
    // FluentAssertion works too ;)
    //result.Should().Not().BeNull();
}

No adapters, no blocking, no hidden sync‑over‑async behavior.

2. Parallel execution by default

TUnit assumes that your tests should run in parallel unless you explicitly say otherwise. This leads to:

  • Faster test suites
  • Early detection of shared‑state issues
  • Better CPU utilization

In xUnit, parallelism is configurable but often constrained by test collections. In TUnit, parallelism is a core principle and is enabled by default.

3. Source generators instead of reflection

Traditional frameworks rely heavily on reflection at runtime to discover and execute tests. TUnit uses C# source generators to:

  • Discover tests at compile time
  • Reduce startup overhead
  • Improve performance and diagnostics

This approach aligns well with modern .NET goals: faster builds, faster test execution, and clearer errors. But also clearer debugging.
If it reminds you of Mapperly vs Automapper, then you know 😁

4. Explicit and predictable lifecycle

TUnit favors explicit composition over magic attributes. Setup, teardown, and dependencies are clear and intentional, making tests easier to understand and maintain, especially in large codebases.


Microsoft.Testing.Platform: The new foundation

To understand why TUnit feels so modern, it helps to look at Microsoft.Testing.Platform, the foundation it is built on.

Microsoft.Testing.Platform is a new, lightweight test execution platform introduced by Microsoft to modernize and gradually replace the legacy VSTest infrastructure. Its role is simple: provide a fast, extensible engine to discover, execute, and report tests, without imposing legacy constraints on test frameworks.

In practice, this means:

  • Faster test discovery and startup

  • A clear separation between test frameworks and test runners

  • Clean support for async and parallel execution

Where older frameworks had to adapt to VSTest’s design, TUnit was created specifically for Microsoft.Testing.Platform. This allows it to avoid heavy runtime reflection, rely on compile‑time discovery via source generators, and integrate cleanly with dotnet test and modern CI pipelines.

Overall, Microsoft.Testing.Platform enables TUnit to be simpler, faster, and more future‑proof than frameworks built on older infrastructure.


Writing Tests: TUnit vs xUnit

Now let’s compare equivalent tests in both frameworks.
We will not be using FluentAssertions in those examples as I assume you may not use it, even if I really recommend it 😌

Basic Test

xUnit

public class CalculatorTests
{
    [Fact]
    public void Add_WhenAddingTwoIntegers_ShouldReturnCorrectResult()
    {
        // Arrange
        var calculator = new Calculator();
        
        // Act
        var result = calculator.Add(2, 3);
        
        // Assert
        Assert.Equal(5, result);
    }
}

TUnit

public class CalculatorTests
{
    [Test]
    public async Task Add_WhenAddingTwoIntegers_ShouldReturnCorrectResult()
    {
        // Arrange
        var calculator = new Calculator();
        
        // Act
        var result = calculator.Add(2, 3);

        // Assert
        await Assert.That(result).IsEqualTo(5);
    }
}

Key differences:

  • TUnit assertions are async and fluent
  • Test methods are async by default
  • No distinction between [Fact] and [Theory]

Parameterized tests

xUnit

[Theory]
[InlineData(1, 2, 3)]
[InlineData(2, 3, 5)]
public void Add_WithMultipleValues_ExpectsCorrectResults(int a, int b, int expected)
{
    // Arrange
    var calculator = new Calculator();
    
    // Act & Assert
    Assert.Equal(expected, calculator.Add(a, b));
}

TUnit

[Test]
[TestCase(1, 2, 3)]
[TestCase(2, 3, 5)]
public async Task Add_WithMultipleValues_ExpectsCorrectResults(int a, int b, int expected)
{
    // Arrange
    var calculator = new Calculator();
    
    // Act & Assert
    await Assert.That(calculator.Add(a, b)).IsEqualTo(expected);
}

TUnit unifies test declaration into a single “Test” prefix model, reducing conceptual overhead.

Test setup and shared context

xUnit (Constructor injection)

public class DatabaseTests : IDisposable
{
    private readonly Database _db = new();

    [Fact]
    public void Insert_WhenInsertingValue_ExpectsCountSuperiorToZero()
    {
        // Act
        _db.Insert("value");
        
        // Assert
        Assert.True(_db.Count > 0);
    }

    public void Dispose() => _db.Dispose();
}

TUnit (Explicit Lifecycle)

public class DatabaseTests
{
    private Database _db = null!;

    [BeforeTest]
    public async Task Setup()
    {
        _db = new Database();
    }

    [Test]
    public async Task Insert_WhenInsertingValue_ExpectsCountSuperiorToZero()
    {
        // Act
        _db.Insert("value");
        
        // Assert
        await Assert.That(_db.Count).IsGreaterThan(0);
    }

    [AfterTest]
    public async Task Cleanup()
    {
        _db.Dispose();
    }
}

TUnit makes lifecycle steps explicit and readable.


About Performance Benchmarks

Despite being an early‑stage framework, TUnit already has publicly available benchmarks on its GitHub .

Here are some of them, the following results come from BenchmarkDotNet v0.15.6, running on Linux Ubuntu 24.04.3 LTS, using .NET 10 (RC2) on an AMD EPYC 7763.

Scenario 1: Async‑Heavy Test Suites

This scenario measures frameworks executing tests that rely heavily on asynchronous operations and async/await patterns.

Framework Version Mean
TUnit 1.0.48 562.4 ms
NUnit 4.4.0 679.2 ms
MSTest 4.0.1 647.7 ms
xUnit3 3.2.0 744.4 ms
TUnit (AOT) 1.0.48 127.6 ms

In async‑heavy scenarios, TUnit already outperforms other mainstream frameworks, while its AOT variant shows a dramatic reduction in execution time by eliminating most runtime overhead.

Scenario 2: Parameterized Tests with Multiple Test Cases

This benchmark focuses on parameterized tests using data attributes, a pretty common pattern in our solutions.

Framework Version Mean
TUnit 1.0.48 476.97 ms
NUnit 4.4.0 537.80 ms
MSTest 4.0.1 496.84 ms
xUnit3 3.2.0 584.15 ms
TUnit (AOT) 1.0.48 24.65 ms

Here again, TUnit consistently leads among JIT‑based frameworks, while TUnit AOT destroys its opponents completely in this particular test scenario.

How to Interpret These Results

As with any benchmark, results depend on workload and environment.
But the results clearly show that in common test scenarios, TUnit leads the way in terms of performance, especially combined with AOT.
This should gives you a glimpse of what to expect when migrating to this framework 😌


Summary of key differences

Aspect xUnit TUnit
Async‑first Partial Default
Parallel execution Configurable Default
AOT Limited Native 🔥
Test discovery Reflection Source generators 🔥
Assertions Sync (can be overrided with libraries) Async & fluent
Mental model Attribute‑heavy Minimal & explicit
Performance focus Good Excellent by design
Target framework .NET Framework, .NET Core, .NET 5+ .NET 8+

When should you consider using TUnit?

TUnit shines when:

  • You have large test suites where performance matters
  • Your codebase is async‑heavy
  • You want faster tasks in CI
  • You prefer explicit lifecycles over implicit magic

xUnit remains an excellent choice for many projects, especially those with a long history or heavy ecosystem dependencies. But for new projects targeting modern .NET versions, TUnit should be truly considered as a new alternative.


Conclusion

TUnit is not just “another test framework”, it represents a modern rethinking of .NET testing, aligned with how developers write code today: async, parallel, and performance‑aware.

If you’re starting a new .NET project or feeling the limits of traditional frameworks, TUnit is definitely a tool to keep on your radar, and I really recommend you to try it 😊

Happy testing 👨‍💻❤️