· 7 min read

TUnit: A New, Modern, Faster Test Framework for .NET

Learn about TUnit, a modern, faster test framework for .NET

Learn about TUnit, a modern, faster test framework for .NET

In .NET World, there are three most popular and widely used test frameworks: xUnit, NUnit, and MSTest. Although these frameworks are great, in my opinion, they aren’t able to keep up with new features from latest .NET versions. For example, these frameworks are still using Reflection to discover and run tests, which can actually be replaced with Source Generators. One can attribute this to the fact that these frameworks are widely used and have to maintain backward compatibility with older versions of .NET (Source Generators were introduced only in .NET 5).

This is where TUnit comes in. TUnit is a modern, faster test framework for .NET that targets .NET 8 and later versions. In this post, I’ll cover some of the features of TUnit and how it differs from other test frameworks.

A Brief Introduction to TUnit

So, TUnit is a modern, faster test framework for .NET that is completely built on top of Microsoft.Testing.Platform to be performant and more extensible. It also leverages Source Generators to discover and run tests, instead of using Reflection, which helps with faster runtime performance. Most important of all, TUnit doesn’t depend on vstest.console or dotnet test to run tests, rather it uses its own embedded test runner, powered by Microsoft.Testing.Platform.

Features of TUnit

  • Test Executable: TUnit generates a test executable that can be run directly from the command line or from the IDE. This can be directly run on the target platform without any additional dependencies, especially without VSTest.

  • No Reflection: TUnit uses Source Generators to discover and run tests, which makes it faster than other test frameworks that use Reflection. Although you might see a slight increase in build time, the runtime performance is going to be much faster. Go through their README to find some impressive benchmarks!

  • Attribute-Based: TUnit uses attributes to perform source generation. Most of the attributes are similar to NUnit, so if you are familiar with NUnit, you’ll feel right at home with TUnit. You can see their full list of attributes here.

  • Test Parallelization: By design, TUnit runs all tests in parallel, even if they are in the same class. This can help you run tests faster, especially if you have a lot of tests in your test suite.

  • Test Dependencies: TUnit allows you to specify test dependencies using [DependsOn(...)], which can be used to run tests in a specific order. This can be useful if you have tests that depend on each other. TUnit will automatically run the dependent tests before running the actual test.

  • Support for Dependency Injection: If you are coming from xUnit, you might be familiar with the concept of constructor injection. TUnit supports this as well, although it isn’t as straightforward as xUnit. You have to implement DependencyInjectionDataSourceAttribute<TScope> to specify TUnit on how to create a Dependency injection scope. This is a bit more complex than xUnit, but it allows you to have more control over the dependency injection process and you can choose any DI container you want.

  • NativeAOT Support: Since TUnit is completely source-generated, it can be used build single-file executables using NativeAOT. This can be useful if you want to build self-contained executables for your tests. However, you need to ensure that your actual code is also compatible with NativeAOT.

  • Built on Microsoft.Testing.Platform: TUnit is built on top of Microsoft.Testing.Platform, which is a modern, performant test platform for .NET. Unsurprisingly, Microsoft is intending to replace older VSTest with newer Microsoft.Testing.Platform in the long run. So any future improvements to Microsoft.Testing.Platform will directly benefit TUnit and other test frameworks built on top of it.

  • Support for Playwright: TUnit also provides support for Playwright through TUnit.Playwright package. This allows you to write end-to-end tests using Playwright and run them using TUnit. This is a great addition if you are using Playwright for your end-to-end tests.

  • IDE Support: TUnit is now supported out-of-box from Visual Studio 2022 17.13 and later versions as well as in Visual Studio Code (through C# Dev Kit extension). TUnit also supports dotnet CLI fully. For Rider, you have to manually enable “Testing Platform Support” in VSTest settings.

Sneak Peek

Here is a small snippet of how a test looks like in TUnit: (Which I am shamelessly copying from their README)

private static readonly TimeOnly Midnight = TimeOnly.FromTimeSpan(TimeSpan.Zero);
private static readonly TimeOnly Noon = TimeOnly.FromTimeSpan(TimeSpan.FromHours(12));

[Test]
public async Task IsMorning()
{
    var time = GetTime();

    await Assert.That(time).IsAfterOrEqualTo(Midnight)
        .And.IsBefore(Noon);
}

Comparison with Other Test Frameworks

Here is a quick comparison of TUnit with other popular test frameworks:

FeatureTUnitxUnitNUnitMSTest
Supported Platforms.NET 8+.NET 4.5+.NET 4.0+.NET 4.5+
Test ExecutableYesYes(In v3)Needs Microsoft.Testing.PlatformNeeds Microsoft.Testing.Platform
Test ParallelizationMethod-LevelClass-LevelClass-LevelMethod-Level
Source GeneratorsYesNoNoNo
NativeAOT SupportYesNoNoNo

Caveats

Although TUnit is a great test framework, there are a few caveats that you should be aware of:

  • No Support for .NET Framework & before .NET 8: TUnit is only supported on .NET 8 and later versions. If you are using .NET Framework or an older version of .NET, you won’t be able to use TUnit. This is a major drawback if you are working on a legacy project that is still using .NET Framework.

  • Still in Preview: TUnit is still in preview and according to the maintainer, the APIs are not marked stable yet, although they are mostly feature-complete. To install TUnit, you need to use --prerelease flag while using dotnet add package command.

  • Migration: Although there is a migration guide available for migrating from xUnit to TUnit, it is not very straightforward at the moment. The migration guide is available here. For NUnit and MSTest, there is no migration guide available yet.

  • Trimming in your actual code: If you are planning to use NativeAOT with TUnit, you need to ensure that your actual code is compatible with NativeAOT. If your application is not compatible with NativeAOT, you won’t be able to use TUnit with NativeAOT. This is a major drawback if you are using any third-party libraries that are not compatible with NativeAOT.

  • Coverage: Currently, TUnit isn’t compatible with Coverlet, which is a popular code coverage tool for .NET. So if you are using Coverlet, you have to rely on Microsoft.Testing.Extensions.CodeCoverage package, as coverlet.collector is specifically designed for older VSTest.

Also note that there are few opioniated features in TUnit that you might find controversial. One example is chaining the tests using [DependsOn(...)] attribute, which some people consider as an anti-pattern, as tests are supposed to be independent of each other. However, TUnit allows you to do this if you want to, but it is not recommended. For example:

[Test]
public async Task SomeIntegrationTest1()
{
    ...
}

[Test, DependsOn(nameof(SomeIntegrationTest1))]
public async Task SomeIntegrationTest2()
{
    ...
}

Another example is that the built-in assertions are async in nature, which can be a source of confusion for some developers, although there is a built-in analyzer to point out any missing await keywords. For example, the following test would be incorrect:


// Wrong way of writing tests
[Test]
public void Multiply_Should_Return_Product() // Doesn't return Task
{
    // Arrange
    var value1 = 7;
    var value2 = 9;
    var expectedAnswer = 63;

    // Act
    var answer = Multiply(value1, value2);

    // Assert
    Assert.That(answer).IsEqualTo(63); // -> This would be error
}

and the correct way would be:


// Correct way of writing tests
[Test]
public async Task Multiply_Should_Return_Product() // Returns Task
{
    // Arrange
    var value1 = 7;
    var value2 = 9;
    var expectedAnswer = 63;

    // Act
    var answer = Multiply(value1, value2);

    // Assert
    await Assert.That(answer).IsEqualTo(63); // -> This would be correct
}

Like any other test frameworks, TUnit still allows you to bring your own assertions, so you can use Shouldly or any other assertion library if you want to.

To solve some of these issues, TUnit provides a built-in analyzer to provide warnings and errors for common mistakes. For example, it would complain if any of the tests are uses async void instead of async Task.

Wrapping Up

For me, TUnit is an extremely promising test framework in terms of performance and extensibility. Given that Microsoft is heavily investing in Microsoft.Testing.Platform and TUnit has first-class support for the new testing platform, I believe TUnit will only get better in the future. It has also received a mention in a recent blog from Microsoft DevBlogs here. It may not reach the same level of adoption as xUnit or NUnit as they are widely used across lot of existing projects and is very recent, but it’s definitely worth giving a try if you are starting a new project or looking to improve the performance of your existing tests.

Give TUnit a try and let me know your thoughts in the comments below!

Back to Blog

Related Posts

View All Posts »
Writing Tests for Dapper with TestContainers in xUnit

Writing Tests for Dapper with TestContainers in xUnit

Writing tests for Dapper is traditionally challenging due to two reasons. In this post, we will explore how to write integration test fixtures for Dapper queries using TestContainers in xUnit and how to reuse the database container across multiple tests.