Migrating from xUnit.net | TUnit (original) (raw)

Performance Boost

Migrating from xUnit to TUnit can improve test execution speed. Check the benchmarks to see how TUnit compares.

Quick Reference

xUnit TUnit
[Fact] [Test]
[Theory] [Test]
[InlineData(...)] [Arguments(...)]
[MemberData(nameof(...))] [MethodDataSource(nameof(...))]
[ClassData(typeof(...))] [MethodDataSource(nameof(ClassName.Method))]
[Trait("key", "value")] [Property("key", "value")]
IClassFixture [ClassDataSource(Shared = SharedType.PerClass)]
[Collection("name")] [ClassDataSource(Shared = SharedType.Keyed, Key = "name")]
Constructor Constructor or [Before(Test)]
IDisposable IDisposable or [After(Test)]
IAsyncLifetime [Before(Test)] / [After(Test)]
ITestOutputHelper TestContext parameter
Assert.Equal(expected, actual) await Assert.That(actual).IsEqualTo(expected)
Assert.Throws(() => ...) Assert.Throws(() => ...)

Automated Migration with Code Fixers

TUnit includes Roslyn analyzers and code fixers that automate most of the migration work. The TUXU0001 diagnostic identifies xUnit code patterns and provides automatic fixes to convert them to TUnit equivalents.

What gets converted automatically:

The code fixer handles roughly 80-90% of typical test suites automatically.

What requires manual adjustment:

If you find a common pattern that should be automated but isn't, please open an issue.

Prerequisites

Step-by-Step Migration

Safety First

Commit your changes or create a backup before running the code fixer. This allows you to review changes and revert if needed.

1. Install TUnit packages

Add the TUnit packages to your test project alongside xUnit (temporarily):

2. Disable TUnit's implicit usings (temporary)

Add these properties to your .csproj to prevent type name conflicts between xUnit and TUnit:


<PropertyGroup>

    <TUnitImplicitUsings>false</TUnitImplicitUsings>

    <TUnitAssertionsImplicitUsings>false</TUnitAssertionsImplicitUsings>

</PropertyGroup>

This allows the code fixer to distinguish between Xunit.Assert and TUnit.Assertions.Assert.

3. Rebuild the project

This restores packages and loads the TUnit analyzers.

Optional: Verify analyzer is working

The TUXU0001 diagnostic is information-level and won't appear in standard build output. If you want to verify the analyzer is detecting xUnit code before applying changes, run:


dotnet format analyzers --severity info --diagnostics TUXU0001 --verify-no-changes

This command checks for TUXU0001 diagnostics without modifying any files. If xUnit code is detected, you'll see messages like "Would fix N files" or specific file paths that would be changed.

4. Run the automated code fixer


dotnet format analyzers --severity info --diagnostics TUXU0001

This command applies all available fixes for the TUXU0001 diagnostic. You'll see output indicating which files were modified.

Multi-targeting Projects

If your project targets multiple .NET versions (e.g., net8.0;net9.0;net10.0), you must specify a single target framework when running the code fixer. Multi-targeting can cause the code fixer to crash with the error Changes must be within bounds of SourceText due to a limitation in Roslyn's linked file handling.

Option 1: Specify a single framework via command line:


dotnet format analyzers --severity info --diagnostics TUXU0001 --framework net10.0

Option 2: Temporarily modify your project file to single-target:


<!-- Before migration -->

<TargetFramework>net10.0</TargetFramework>

<!-- <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks> -->

Run the code fixer, then restore multi-targeting afterward. Replace net10.0 with your project's highest supported target framework.

5. Remove the implicit usings workaround

Remove or comment out the properties you added in step 2:


<!-- Remove these lines -->

<PropertyGroup>

    <TUnitImplicitUsings>false</TUnitImplicitUsings>

    <TUnitAssertionsImplicitUsings>false</TUnitAssertionsImplicitUsings>

</PropertyGroup>

6. Fix remaining issues manually

Build the project and address any remaining compilation errors:

Common manual fixes needed:

7. Remove xUnit packages

Once everything compiles and tests pass:


dotnet remove package xunit

dotnet remove package xunit.runner.visualstudio

8. Verify the migration


dotnet build

dotnet run -- --list-tests

Troubleshooting

Code fixer doesn't run / no files changed:

Build errors after running code fixer:

IClassFixture not converted:

Analyzers not loading:

Manual Migration Guide

Basic Test Structure

Simple Test (Fact → Test)

xUnit Code:


public class CalculatorTests

{

    [Fact]

    public void Add_TwoNumbers_ReturnsSum()

    {

        var calculator = new Calculator();

        var result = calculator.Add(2, 3);

        Assert.Equal(5, result);

    }

}

TUnit Equivalent:


public class CalculatorTests

{

    [Test]

    public async Task Add_TwoNumbers_ReturnsSum()

    {

        var calculator = new Calculator();

        var result = calculator.Add(2, 3);

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

    }

}

Key Changes:

Parameterized Tests

Theory with InlineData → Arguments

xUnit Code:


public class StringTests

{

    [Theory]

    [InlineData("hello", 5)]

    [InlineData("world", 5)]

    [InlineData("", 0)]

    public void Length_ReturnsCorrectValue(string input, int expectedLength)

    {

        Assert.Equal(expectedLength, input.Length);

    }

}

TUnit Equivalent:


public class StringTests

{

    [Test]

    [Arguments("hello", 5)]

    [Arguments("world", 5)]

    [Arguments("", 0)]

    public async Task Length_ReturnsCorrectValue(string input, int expectedLength)

    {

        await Assert.That(input.Length).IsEqualTo(expectedLength);

    }

}

Key Changes:

Data Sources

MemberData → MethodDataSource

xUnit Code:


public class DataDrivenTests

{

    [Theory]

    [MemberData(nameof(GetTestData))]

    public void ProcessData_WithVariousInputs(int value, string text, bool expected)

    {

        var result = SomeLogic(value, text);

        Assert.Equal(expected, result);

    }

    public static IEnumerable<object[]> GetTestData()

    {

        yield return new object[] { 1, "test", true };

        yield return new object[] { 2, "demo", false };

        yield return new object[] { 3, "example", true };

    }

}

TUnit Equivalent:


public class DataDrivenTests

{

    [Test]

    [MethodDataSource(nameof(GetTestData))]

    public async Task ProcessData_WithVariousInputs(int value, string text, bool expected)

    {

        var result = SomeLogic(value, text);

        await Assert.That(result).IsEqualTo(expected);

    }

    public static IEnumerable<(int value, string text, bool expected)> GetTestData()

    {

        yield return (1, "test", true);

        yield return (2, "demo", false);

        yield return (3, "example", true);

    }

}

Key Changes:

ClassData → MethodDataSource

xUnit Code:


public class TestDataGenerator : IEnumerable<object[]>

{

    public IEnumerator<object[]> GetEnumerator()

    {

        yield return new object[] { 1, "one" };

        yield return new object[] { 2, "two" };

    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

}

public class MyTests

{

    [Theory]

    [ClassData(typeof(TestDataGenerator))]

    public void TestWithClassData(int number, string text)

    {

        Assert.NotNull(text);

    }

}

TUnit Equivalent:


public class MyTests

{

    [Test]

    [MethodDataSource(nameof(TestDataGenerator.GetTestData))]

    public async Task TestWithClassData(int number, string text)

    {

        await Assert.That(text).IsNotNull();

    }

}

public class TestDataGenerator

{

    public static IEnumerable<(int, string)> GetTestData()

    {

        yield return (1, "one");

        yield return (2, "two");

    }

}

Key Changes:

Setup and Teardown

Constructor and IDisposable → Before/After Hooks

xUnit Code:


public class DatabaseTests : IDisposable

{

    private readonly DatabaseConnection _connection;

    public DatabaseTests()

    {

        _connection = new DatabaseConnection();

        _connection.Open();

    }

    [Fact]

    public void Query_ReturnsData()

    {

        var result = _connection.Query("SELECT * FROM Users");

        Assert.NotNull(result);

    }

    public void Dispose()

    {

        _connection?.Close();

        _connection?.Dispose();

    }

}

TUnit Equivalent (Option 1: Using IDisposable):


public class DatabaseTests : IDisposable

{

    private DatabaseConnection _connection = null!;

    public DatabaseTests()

    {

        _connection = new DatabaseConnection();

        _connection.Open();

    }

    [Test]

    public async Task Query_ReturnsData()

    {

        var result = _connection.Query("SELECT * FROM Users");

        await Assert.That(result).IsNotNull();

    }

    public void Dispose()

    {

        _connection?.Close();

        _connection?.Dispose();

    }

}

TUnit Equivalent (Option 2: Using Hooks):


public class DatabaseTests

{

    private DatabaseConnection _connection = null!;

    [Before(Test)]

    public async Task Setup()

    {

        _connection = new DatabaseConnection();

        await _connection.OpenAsync();

    }

    [Test]

    public async Task Query_ReturnsData()

    {

        var result = _connection.Query("SELECT * FROM Users");

        await Assert.That(result).IsNotNull();

    }

    [After(Test)]

    public async Task Cleanup()

    {

        if (_connection != null)

        {

            await _connection.CloseAsync();

            _connection.Dispose();

        }

    }

}

Key Changes:

IAsyncLifetime → Before/After Hooks

xUnit Code:


public class AsyncSetupTests : IAsyncLifetime

{

    private HttpClient _client = null!;

    public async Task InitializeAsync()

    {

        _client = new HttpClient();

        await _client.GetAsync("https://api.example.com/warm-up");

    }

    [Fact]

    public async Task FetchData_ReturnsSuccess()

    {

        var response = await _client.GetAsync("https://api.example.com/data");

        Assert.True(response.IsSuccessStatusCode);

    }

    public async Task DisposeAsync()

    {

        _client?.Dispose();

        await Task.CompletedTask;

    }

}

TUnit Equivalent:


public class AsyncSetupTests

{

    private HttpClient _client = null!;

    [Before(Test)]

    public async Task Setup()

    {

        _client = new HttpClient();

        await _client.GetAsync("https://api.example.com/warm-up");

    }

    [Test]

    public async Task FetchData_ReturnsSuccess()

    {

        var response = await _client.GetAsync("https://api.example.com/data");

        await Assert.That(response.IsSuccessStatusCode).IsTrue();

    }

    [After(Test)]

    public async Task Cleanup()

    {

        _client?.Dispose();

    }

}

Key Changes:

Shared Context and Fixtures

IClassFixture → ClassDataSource

xUnit Code:


public class DatabaseFixture : IDisposable

{

    public DatabaseConnection Connection { get; }

    public DatabaseFixture()

    {

        Connection = new DatabaseConnection();

        Connection.Open();

    }

    public void Dispose()

    {

        Connection?.Close();

        Connection?.Dispose();

    }

}

public class UserRepositoryTests : IClassFixture<DatabaseFixture>

{

    private readonly DatabaseFixture _fixture;

    public UserRepositoryTests(DatabaseFixture fixture)

    {

        _fixture = fixture;

    }

    [Fact]

    public void GetUser_ReturnsUser()

    {

        var repo = new UserRepository(_fixture.Connection);

        var user = repo.GetUser(1);

        Assert.NotNull(user);

    }

    [Fact]

    public void GetAllUsers_ReturnsUsers()

    {

        var repo = new UserRepository(_fixture.Connection);

        var users = repo.GetAllUsers();

        Assert.NotEmpty(users);

    }

}

TUnit Equivalent:


public class DatabaseFixture : IDisposable

{

    public DatabaseConnection Connection { get; }

    public DatabaseFixture()

    {

        Connection = new DatabaseConnection();

        Connection.Open();

    }

    public void Dispose()

    {

        Connection?.Close();

        Connection?.Dispose();

    }

}

[ClassDataSource<DatabaseFixture>(Shared = SharedType.PerClass)]

public class UserRepositoryTests(DatabaseFixture fixture)

{

    [Test]

    public async Task GetUser_ReturnsUser()

    {

        var repo = new UserRepository(fixture.Connection);

        var user = repo.GetUser(1);

        await Assert.That(user).IsNotNull();

    }

    [Test]

    public async Task GetAllUsers_ReturnsUsers()

    {

        var repo = new UserRepository(fixture.Connection);

        var users = repo.GetAllUsers();

        await Assert.That(users).IsNotEmpty();

    }

}

Key Changes:

Collection Fixtures → Shared ClassDataSource

xUnit Code:


[CollectionDefinition("Database collection")]

public class DatabaseCollection : ICollectionFixture<DatabaseFixture>

{

}

public class DatabaseFixture : IDisposable

{

    public DatabaseConnection Connection { get; }

    public DatabaseFixture()

    {

        Connection = new DatabaseConnection();

        Connection.Open();

    }

    public void Dispose() => Connection?.Dispose();

}

[Collection("Database collection")]

public class UserTests : IClassFixture<DatabaseFixture>

{

    private readonly DatabaseFixture _fixture;

    public UserTests(DatabaseFixture fixture)

    {

        _fixture = fixture;

    }

    [Fact]

    public void CreateUser_Succeeds()

    {

        // Test using _fixture.Connection

    }

}

[Collection("Database collection")]

public class ProductTests : IClassFixture<DatabaseFixture>

{

    private readonly DatabaseFixture _fixture;

    public ProductTests(DatabaseFixture fixture)

    {

        _fixture = fixture;

    }

    [Fact]

    public void CreateProduct_Succeeds()

    {

        // Test using _fixture.Connection

    }

}

TUnit Equivalent:


public class DatabaseFixture : IDisposable

{

    public DatabaseConnection Connection { get; }

    public DatabaseFixture()

    {

        Connection = new DatabaseConnection();

        Connection.Open();

    }

    public void Dispose() => Connection?.Dispose();

}

[ClassDataSource<DatabaseFixture>(Shared = SharedType.Keyed, Key = "DatabaseCollection")]

public class UserTests(DatabaseFixture fixture)

{

    [Test]

    public async Task CreateUser_Succeeds()

    {

        // Test using fixture.Connection

    }

}

[ClassDataSource<DatabaseFixture>(Shared = SharedType.Keyed, Key = "DatabaseCollection")]

public class ProductTests(DatabaseFixture fixture)

{

    [Test]

    public async Task CreateProduct_Succeeds()

    {

        // Test using fixture.Connection

    }

}

Key Changes:

Assembly Fixture → ClassDataSource with PerAssembly

xUnit doesn't have native assembly fixtures, but TUnit does:

TUnit Example:


public class ApplicationFixture : IDisposable

{

    public IServiceProvider ServiceProvider { get; }

    public ApplicationFixture()

    {

        // Setup once for entire assembly

        ServiceProvider = ConfigureServices();

    }

    public void Dispose()

    {

        // Cleanup once after all tests

    }

}

[ClassDataSource<ApplicationFixture>(Shared = SharedType.PerAssembly)]

public class IntegrationTests(ApplicationFixture fixture)

{

    [Test]

    public async Task Test1()

    {

        var service = fixture.ServiceProvider.GetService<IMyService>();

        await Assert.That(service).IsNotNull();

    }

}

Test Output

ITestOutputHelper → TestContext

xUnit Code:


public class LoggingTests

{

    private readonly ITestOutputHelper _output;

    public LoggingTests(ITestOutputHelper output)

    {

        _output = output;

    }

    [Fact]

    public void Test_WithLogging()

    {

        _output.WriteLine("Starting test");

        var result = PerformOperation();

        _output.WriteLine($"Result: {result}");

        Assert.True(result > 0);

    }

}

TUnit Equivalent:


public class LoggingTests

{

    [Test]

    public async Task Test_WithLogging(TestContext context)

    {

        context.Output.WriteLine("Starting test");

        var result = PerformOperation();

        context.Output.WriteLine($"Result: {result}");

        await Assert.That(result).IsGreaterThan(0);

    }

}

Key Changes:

Test Attachments

xUnit v3 introduced test attachments. TUnit also supports this capability:

xUnit v3 Code:


public class TestWithAttachments

{

    private readonly ITestContextAccessor _testContextAccessor;

    public TestWithAttachments(ITestContextAccessor testContextAccessor)

    {

        _testContextAccessor = testContextAccessor;

    }

    [Fact]

    public async Task Test_WithAttachment()

    {

        // Test logic

        var logPath = "test-log.txt";

        await File.WriteAllTextAsync(logPath, "test logs");

        _testContextAccessor.Current!.Attachments.Add(

            new FileAttachment(logPath, "Test Log"));

    }

}

TUnit Equivalent:


public class TestWithAttachments

{

    [Test]

    public async Task Test_WithAttachment()

    {

        // Test logic

        var logPath = "test-log.txt";

        await File.WriteAllTextAsync(logPath, "test logs");

        TestContext.Current!.Output.AttachArtifact(new Artifact

        {

            File = new FileInfo(logPath),

            DisplayName = "Test Log",

            Description = "Logs captured during test execution"  // Optional

        });

    }

}

For more information about working with test artifacts, including session-level artifacts and best practices, see the Test Artifacts guide.

Traits and Categories

Trait → Property

xUnit Code:


public class FeatureTests

{

    [Fact]

    [Trait("Category", "Integration")]

    [Trait("Priority", "High")]

    public void ImportantIntegrationTest()

    {

        // Test implementation

    }

}

TUnit Equivalent:


public class FeatureTests

{

    [Test]

    [Property("Category", "Integration")]

    [Property("Priority", "High")]

    public async Task ImportantIntegrationTest()

    {

        // Test implementation

    }

}

Key Changes:

Assertions

Basic Assertions

xUnit Code:


[Fact]

public void Assertions_Examples()

{

    Assert.Equal(5, 2 + 3);

    Assert.NotEqual(5, 2 + 2);

    Assert.True(5 > 3);

    Assert.False(5 < 3);

    Assert.Null(null);

    Assert.NotNull("value");

    Assert.Same(obj1, obj2);

    Assert.NotSame(obj1, obj3);

}

TUnit Equivalent:


[Test]

public async Task Assertions_Examples()

{

    await Assert.That(2 + 3).IsEqualTo(5);

    await Assert.That(2 + 2).IsNotEqualTo(5);

    await Assert.That(5 > 3).IsTrue();

    await Assert.That(5 < 3).IsFalse();

    await Assert.That((object?)null).IsNull();

    await Assert.That("value").IsNotNull();

    await Assert.That(obj1).IsSameReferenceAs(obj2);

    await Assert.That(obj1).IsNotSameReferenceAs(obj3);

}

Collection Assertions

xUnit Code:


[Fact]

public void Collection_Assertions()

{

    var list = new[] { 1, 2, 3 };

    Assert.Contains(2, list);

    Assert.DoesNotContain(5, list);

    Assert.Empty(Array.Empty<int>());

    Assert.NotEmpty(list);

    Assert.Equal(3, list.Length);

}

TUnit Equivalent:


[Test]

public async Task Collection_Assertions()

{

    var list = new[] { 1, 2, 3 };

    await Assert.That(list).Contains(2);

    await Assert.That(list).DoesNotContain(5);

    await Assert.That(Array.Empty<int>()).IsEmpty();

    await Assert.That(list).IsNotEmpty();

    await Assert.That(list).Count().IsEqualTo(3);

}

String Assertions

xUnit Code:


[Fact]

public void String_Assertions()

{

    var text = "Hello, World!";

    Assert.Contains("World", text);

    Assert.DoesNotContain("xyz", text);

    Assert.StartsWith("Hello", text);

    Assert.EndsWith("!", text);

    Assert.Matches(@"H\w+", text);

}

TUnit Equivalent:


[Test]

public async Task String_Assertions()

{

    var text = "Hello, World!";

    await Assert.That(text).Contains("World");

    await Assert.That(text).DoesNotContain("xyz");

    await Assert.That(text).StartsWith("Hello");

    await Assert.That(text).EndsWith("!");

    await Assert.That(text).Matches(@"H\w+");

}

Exception Assertions

xUnit Code:


[Fact]

public void Exception_Assertions()

{

    Assert.Throws<ArgumentException>(() => ThrowsException());

    var ex = Assert.Throws<ArgumentException>(() => ThrowsException());

    Assert.Equal("paramName", ex.ParamName);

}

[Fact]

public async Task Async_Exception_Assertions()

{

    await Assert.ThrowsAsync<InvalidOperationException>(() => ThrowsExceptionAsync());

}

TUnit Equivalent:


[Test]

public async Task Exception_Assertions()

{

    Assert.Throws<ArgumentException>(() => ThrowsException());

    var ex = Assert.Throws<ArgumentException>(() => ThrowsException());

    await Assert.That(ex.ParamName).IsEqualTo("paramName");

}

[Test]

public async Task Async_Exception_Assertions()

{

    await Assert.ThrowsAsync<InvalidOperationException>(() => ThrowsExceptionAsync());

}

Key Changes:

Complete Example: Real-World Test Class

xUnit Code:


public class UserServiceTests : IClassFixture<DatabaseFixture>, IAsyncLifetime

{

    private readonly DatabaseFixture _dbFixture;

    private readonly ITestOutputHelper _output;

    private UserService _userService = null!;

    public UserServiceTests(DatabaseFixture dbFixture, ITestOutputHelper output)

    {

        _dbFixture = dbFixture;

        _output = output;

    }

    public async Task InitializeAsync()

    {

        _userService = new UserService(_dbFixture.Connection);

        await _userService.InitializeAsync();

    }

    public Task DisposeAsync() => Task.CompletedTask;

    [Theory]

    [InlineData("john@example.com", "John")]

    [InlineData("jane@example.com", "Jane")]

    public async Task CreateUser_WithValidData_Succeeds(string email, string name)

    {

        _output.WriteLine($"Creating user: {name}");

        var user = await _userService.CreateUserAsync(email, name);

        Assert.NotNull(user);

        Assert.Equal(email, user.Email);

        Assert.Equal(name, user.Name);

        _output.WriteLine($"User created with ID: {user.Id}");

    }

    [Fact]

    public async Task GetUser_WhenNotFound_ThrowsException()

    {

        await Assert.ThrowsAsync<UserNotFoundException>(

            () => _userService.GetUserAsync(99999));

    }

    [Theory]

    [MemberData(nameof(GetInvalidEmails))]

    public async Task CreateUser_WithInvalidEmail_ThrowsException(string invalidEmail)

    {

        await Assert.ThrowsAsync<ArgumentException>(

            () => _userService.CreateUserAsync(invalidEmail, "Test"));

    }

    public static IEnumerable<object[]> GetInvalidEmails()

    {

        yield return new object[] { "" };

        yield return new object[] { "not-an-email" };

        yield return new object[] { "@example.com" };

    }

}

TUnit Equivalent:


[ClassDataSource<DatabaseFixture>(Shared = SharedType.PerClass)]

public class UserServiceTests(DatabaseFixture dbFixture)

{

    private UserService _userService = null!;

    [Before(Test)]

    public async Task Setup()

    {

        _userService = new UserService(dbFixture.Connection);

        await _userService.InitializeAsync();

    }

    [Test]

    [Arguments("john@example.com", "John")]

    [Arguments("jane@example.com", "Jane")]

    public async Task CreateUser_WithValidData_Succeeds(string email, string name, TestContext context)

    {

        context.Output.WriteLine($"Creating user: {name}");

        var user = await _userService.CreateUserAsync(email, name);

        await Assert.That(user).IsNotNull();

        await Assert.That(user.Email).IsEqualTo(email);

        await Assert.That(user.Name).IsEqualTo(name);

        context.Output.WriteLine($"User created with ID: {user.Id}");

    }

    [Test]

    public async Task GetUser_WhenNotFound_ThrowsException()

    {

        await Assert.ThrowsAsync<UserNotFoundException>(

            () => _userService.GetUserAsync(99999));

    }

    [Test]

    [MethodDataSource(nameof(GetInvalidEmails))]

    public async Task CreateUser_WithInvalidEmail_ThrowsException(string invalidEmail)

    {

        await Assert.ThrowsAsync<ArgumentException>(

            () => _userService.CreateUserAsync(invalidEmail, "Test"));

    }

    public static IEnumerable<string> GetInvalidEmails()

    {

        yield return "";

        yield return "not-an-email";

        yield return "@example.com";

    }

}

Key Differences Summary:

Code Coverage

TUnit includes built-in code coverage support. Do not use Coverlet — it is incompatible with TUnit's Microsoft.Testing.Platform.

See the Code Coverage guide for setup and configuration.