xUnit v3 and some stuff about TUnit

9/30/2024

I know that there is a lot of fuse about TUnit and I am here writing about xUnit. I might cover TUnit in the future, but for now, the topic is the v3 prerelease of xUnit!

v3

v3 is in the making since a while now and it is still in the prerelease stage. The main goal of v3 is to drop old stuff that was needed in the "good old days" and to make the framework more modern. I recently started to upgrade this very blog to use v3 and I am very happy with the results. While there are new features, I didn't really used them until now. Anyway, let's highlight a few things here.

Executable!

If you want to play around, you can easily install the template:

dotnet new install xunit.v3.templates

And create a new project:

dotnet new xunit3

The first thing you will notice is something like this inside the created csproj:

<OutputType>Exe</OutputType>

Yes, the test project is now an executable! So test runner and test project are the same thing! So you can run your tests like this:

dotnet run

For example the unit tests of this blog post will print something like this:

$ dotnet run
xUnit.net v3 In-Process Runner v0.4.0-pre.20+7ef620d780 (64-bit .NET 9.0.0-rc.1.24431.7)
  Discovering: LinkDotNet.Blog.UnitTests
  Discovered:  LinkDotNet.Blog.UnitTests
  Starting:    LinkDotNet.Blog.UnitTests
  Finished:    LinkDotNet.Blog.UnitTests
=== TEST EXECUTION SUMMARY ===
   LinkDotNet.Blog.UnitTests  Total: 246, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0, Time: 0.350s

You can also print out which test-classes, traits and what not you have via the command line:

dotnet run -- -list classes

The extra -- between run and -list is needed to pass the arguments to the test runner and not the dotnet cli itself. In theory you can also create a self-contained executable and run it on a different machine without the need of the dotnet cli at all! Hello TUnit!

Migration

The migration (at least for me) was straight forward. Instead of the xunit package reference you have to use xunit.v3 - they wanted to make it very explicit! And as discussed earlier, I switched from library (which is the default if you don't set any OutputType) to executable. But as of now, you don't have to do this. You can still use the library output type - but there isn't really a reason to do so.

A very detailed migration guide can be found here.

Cancellation Tokens

A pretty neat feature is the support for cancellation tokens that are passed to the test as TestContext.Current.CancellationToken:

await Task.Delay(1000, TestContext.Current.CancellationToken);

This makes it more convenient to cancel async things if you have to! The analyzers will automatically "inform" you if you are not using the token.

Matrix theory data

Also hello TUnit! Version 3 brings also support for defining theory data in a matrix style. This is a feature that is also available in TUnit and it is very handy if you have a lot of data to test. Here is an example (shamelessly stolen from "What's New"):

public static TheoryData<int, string> MyData =
    new MatrixTheoryData<int, string>(
        [42, 2112, 2600],
        ["Hello", "World"]
    );

Which will result in 6 tests:

  • 42, "Hello"
  • 42, "World"
  • 2112, "Hello"
  • 2112, "World"
  • 2600, "Hello"
  • 2600, "World"

IAsyncLifetime

A small quality of life "fix" is that IAsyncLifetime directly inherits from IAsyncDisposable. This makes it easier to implement the interface and you don't have to implement DisposeAsync anymore "twice". If you only need to disüose of things, IAsyncDisposable is enough! You don't have to implement IAsyncLifetime anymore. This is only useful, if you need an InitializeAsync call!

Conclusion and will I switch to TUnit?

I am very happy with the new features of v3 and I will definitely migrate more of my projects to it. Will I switch to TUnit? For now: No. And the main reason is simple: I love the simplicity and to a big chunk the idiology behind xUunit. Look at the following test in TUnit:

[DependsOn(nameof(Test1), [typeof(string), typeof(int)])]
public void Test2() { ... }

DependsOn is an explicit feature. I don't like that tests depend on other tests. I do see some value - for example if you have Playwright e2e tests where it is expensive to setup everything for every test case. But besides that, I see a a BIG area of misuse. Of course I cherry-picked this example, and there are many things that are very nice! Let's see where this is headed and maybe in a few months I will switch to TUnit.

Performance (ReadOnly)List vs Immutable collection types

A bit back on LinkedIn, there was a discussion about read-only collection and immutability where this is not the point I want to discuss now, as I already covered that here: "ReadOnlyCollection is not an immutable collection".

This post is just about the performance of those types compared to our baseline, the good old List<T>. It also explains why we see the results we see.

Typesafety in xUnit with TheoryData<T>

I recently discovered this small but very useful utility in xUnit: TheoryData<T>.

Why I like and prefer xUnit

In almost all of my projects, I only use xUnit, and here is a small love letter. Especially the one fact I do think makes it a good choice!

An error has occurred. This application may no longer respond until reloaded. Reload x