TUnit “A modern testing framework for .NET built with performance in mind.” as they state in their website is a newcomer in dotnet testing libraries. The initial commit to the TUnit GitHub repository was made at Jan 24, 2024. Since then there has been over 4000 commits and 200+ releases.
According to TUnit, one of the key advantages over traditional frameworks such as xUnit and NUnit is that tests run in parallel by default, it has compile-time generation (which we will alter look more into it) and that it has Native AOT support. Well these are all great features, but personally I think they are not enough to make me swap from xUnit or NUnit into TUnit. If you are building a greenfield project, then TUnit could (and should) be a viable option. Let’s see how it works in practice.
Writing Tests
One cool thing that TUnit offers compared to rivals is that all tests are marked with [Test] attribute. For example in xUnit you have to use [Fact] and [Theory] depending on how you are going to handle test parameters. I have to say that I like this one attribute approach much more. TUnit has all the basic (and advanced) features that you are expecting from a modern unit testing library: It supports skipping tests, giving test parameters as arguments, test lifecycle management, execution control, assertions, customization and much more. I don’t think you will very easily run out of features with TUnit.
TUnit has also some new tricks in its sleeves and one of them is DependsOn. DependsOn attribute allows tests to depend on each other. At first glance, this might seem to violate the Single Responsibility Principle and several other best practices. However, in real-world scenarios, there are times when you need tests to run in a specific order. For example, you might need to execute an Add test before a Delete test, or call certain methods to initialize the test environment. Normally, you’d handle this with setup or initialization methods—but what if the setup itself needs to be tested?
Assertion
The actual unit test part without any fancy bells and whistles is quite boring. You add [Test] attribute over test methods and maybe decorate them with [Arguments] if you want to build tests for different parameter values. Assertion is done in fluent syntax Assert.That(target).IsEqualTo(expected value). You can also use fluent syntax to add Because, And, Or, Within and IgnoreType functionalities to assertion. I personally think that this fluent syntax looks nice and it is easier to read than xUnit’s Assert.Equal(3.142, actualPi, 3) kind of syntax. It might require more key strokes, but that hasn’t been a problem since 80’s(?).
There is however one thing that you must think about in these modern times: LLM’s are much more proficient with xUnit and NUnit than with new TUnit. If you use AI a lot you will struggle more with TUnit than with its older siblings.
Here is a sample of how to do unit testing with TUnit.
public class CalculatorTests
{
// Basic test demonstrating TUnit usage.
// Run with: dotnet test
[Test]
public async Task Add_AddsTwoNumbers()
{
var result = Calculator.Add(2, 3);
await Assert.That(result).IsEqualTo(5);
}
[Test]
[Arguments(1, 2, 3)]
public async Task Add_AddsTwoNumbers(int a, int b, int expected)
{
var result = Calculator.Add(a, b);
await Assert.That(result).IsEqualTo(expected).Because("Addition should work correctly");
}
[Test]
public async Task Multiply_MultipliesTwoNumbers()
{
var result = Calculator.Multiply(4, 6);
await Assert.That(result).IsEqualTo(24);
}
[Test]
[DependsOn("Add_AddsTwoNumbers")]
public async Task Divide_DividesTwoNumbers()
{
var result = Calculator.Divide(10, 2);
await Assert.That(result).IsEqualTo(5).Because($"Division should work correctly and should be run after {nameof(Add_AddsTwoNumbers)}");
}
}
Compile-time generation
TUnit has one big difference compared to many other test frameworks and that is Compile-time test generation. TUnit prefers Compile-Time test generation over runtime reflection. The good thing about this approach is that you don’t need to scan assemblies and use runtime reflection to discover tests. This makes the test discovery bit faster and it also lowers the memory footprint. Compile-time test generation also reduces overhead during test execution and reduces runtime errors (you might get errors earlier during the compile). There are of course some trade-offs in this approach. The main trade-off is a slightly longer compilation time due to source generation.
.NET Framework
You can use TUnit also in .NET Framework projects, but it has some “weird” solutions around that. .NET Framework does not contain all the classes that TUnit is using so TUnit.Core is references Polyfill Nuget package. This is not a huge thing, but as stated in Tests1774.cs file the extension method resolution will have some issues with Polyfill package. Anyway you can use TUnit with .NET Framework projects also.
Summary
TUnit is the new kid on the block, bringing some fresh ideas to the testing landscape — a welcome breath of fresh air. However, in modern times when we’re more and more dependent on AI, introducing new tools into software development can be challenging. AI tends to hallucinate more with TUnit than with its more established rivals like xUnit and NUnit, simply because there’s far more training material available for those frameworks. Still TUnit is worth considering for greenfield projects, because of its fluent assertion syntax, compile-time test generation, and solid tooling support.

Pingback: Microsoft Test Platform (MTP) - New Way to Run Unit Tests - Panu Oksala
Comments are closed.