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<T> | [ClassDataSource<T>(Shared = SharedType.PerClass)] |
[Collection("name")] | [ClassDataSource<T>(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<T>(() => ...) | Assert.Throws<T>(() => ...) |
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:
[Fact]→[Test][Theory]→[Test][InlineData(...)]→[Arguments(...)][MemberData(nameof(...))]→[MethodDataSource(nameof(...))][Trait("key", "value")]→[Property("key", "value")]Assert.Equal(expected, actual)→await Assert.That(actual).IsEqualTo(expected)Assert.True(condition)→await Assert.That(condition).IsTrue()Assert.Throws<T>(...)→Assert.Throws<T>(...)Assert.Contains(item, collection)→await Assert.That(collection).Contains(item)- Test methods converted to
async Taskwithawaiton assertions
The code fixer handles roughly 80-90% of typical test suites automatically.
What requires manual adjustment:
IClassFixture<T>→[ClassDataSource<T>(Shared = SharedType.PerClass)]on the classICollectionFixture<T>and[Collection("name")]→[ClassDataSource<T>(Shared = SharedType.Keyed, Key = "name")]IAsyncLifetime→[Before(Test)]and[After(Test)]methodsITestOutputHelper→TestContextparameter injection- Custom
MemberDatareturn types (convertIEnumerable<object[]>toIEnumerable<(...)>tuples) [ClassData(typeof(...))]→[MethodDataSource(nameof(ClassName.Method))]- Constructor injection of fixtures → primary constructor with
[ClassDataSource<T>]attribute - Collection definitions → remove
ICollectionFixtureclasses entirely
If you find a common pattern that should be automated but isn't, please open an issue.
Prerequisites
- .NET SDK 8.0 or later (for
dotnet formatwith analyzer support) - TUnit packages installed in your test project
Step-by-Step Migration
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):
dotnet add package TUnit
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
dotnet build
This restores packages and loads the TUnit analyzers.
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.
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:
dotnet build
Common manual fixes needed:
- Replace
IClassFixture<T>with[ClassDataSource<T>(Shared = SharedType.PerClass)]attribute - Replace
IAsyncLifetimewith[Before(Test)]/[After(Test)]methods - Replace
ITestOutputHelperconstructor parameter withTestContextmethod parameter - Convert data source methods to return tuples instead of
object[] - Add
using TUnit.Core;andusing TUnit.Assertions;if not using implicit usings - Remove
ICollectionFixtureand collection definition classes
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:
- Ensure you rebuilt after adding TUnit packages
- Check that
TUXU0001warnings appear in build output - Try running with verbose output:
dotnet format analyzers --severity info --diagnostics TUXU0001 --verbosity detailed
Build errors after running code fixer:
- Missing
awaitkeywords: ensure test methods areasync Task - Ambiguous
Assert: remove xUnit usings or fully qualify types - Type mismatch in data sources: convert
IEnumerable<object[]>returns toIEnumerable<(...)>tuples
IClassFixture not converted:
- This requires manual conversion - add
[ClassDataSource<T>(Shared = SharedType.PerClass)]to the class - Use a primary constructor to receive the fixture:
public class MyTests(MyFixture fixture)
Analyzers not loading:
- Verify TUnit package is installed:
dotnet list package - Try cleaning and rebuilding:
dotnet clean && dotnet build
Manual Migration Guide
Basic Test Structure
Simple Test (Fact → Test)
xUnit Code:
publicclassCalculatorTests
{
[Fact]
publicvoidAdd_TwoNumbers_ReturnsSum()
{
var calculator =newCalculator();
var result = calculator.Add(2,3);
Assert.Equal(5, result);
}
}
TUnit Equivalent:
publicclassCalculatorTests
{
[Test]
publicasyncTaskAdd_TwoNumbers_ReturnsSum()
{
var calculator =newCalculator();
var result = calculator.Add(2,3);
await Assert.That(result).IsEqualTo(5);
}
}
Key Changes:
[Fact]→[Test]- Test method returns
async Task - Assertions use fluent syntax with
await Assert.That(...)
Parameterized Tests
Theory with InlineData → Arguments
xUnit Code:
publicclassStringTests
{
[Theory]
[InlineData("hello",5)]
[InlineData("world",5)]
[InlineData("",0)]
publicvoidLength_ReturnsCorrectValue(string input,int expectedLength)
{
Assert.Equal(expectedLength, input.Length);
}
}
TUnit Equivalent:
publicclassStringTests
{
[Test]
[Arguments("hello",5)]
[Arguments("world",5)]
[Arguments("",0)]
publicasyncTaskLength_ReturnsCorrectValue(string input,int expectedLength)
{
await Assert.That(input.Length).IsEqualTo(expectedLength);
}
}
Key Changes:
[Theory]→[Test][InlineData(...)]→[Arguments(...)]- Method is async and assertions are awaited
Data Sources
MemberData → MethodDataSource
xUnit Code:
publicclassDataDrivenTests
{
[Theory]
[MemberData(nameof(GetTestData))]
publicvoidProcessData_WithVariousInputs(intvalue,string text,bool expected)
{
var result =SomeLogic(value, text);
Assert.Equal(expected, result);
}
publicstaticIEnumerable<object[]>GetTestData()
{
yieldreturnnewobject[]{1,"test",true};
yieldreturnnewobject[]{2,"demo",false};
yieldreturnnewobject[]{3,"example",true};
}
}
TUnit Equivalent:
publicclassDataDrivenTests
{
[Test]
[MethodDataSource(nameof(GetTestData))]
publicasyncTaskProcessData_WithVariousInputs(intvalue,string text,bool expected)
{
var result =SomeLogic(value, text);
await Assert.That(result).IsEqualTo(expected);
}
publicstaticIEnumerable<(intvalue,string text,bool expected)>GetTestData()
{
yieldreturn(1,"test",true);
yieldreturn(2,"demo",false);
yieldreturn(3,"example",true);
}
}
Key Changes:
[MemberData(nameof(...))]→[MethodDataSource(nameof(...))]- Data source returns tuples instead of
object[](strongly typed) - No need for boxing/unboxing values
ClassData → MethodDataSource
xUnit Code:
publicclassTestDataGenerator:IEnumerable<object[]>
{
publicIEnumerator<object[]>GetEnumerator()
{
yieldreturnnewobject[]{1,"one"};
yieldreturnnewobject[]{2,"two"};
}
IEnumerator IEnumerable.GetEnumerator()=>GetEnumerator();
}
publicclassMyTests
{
[Theory]
[ClassData(typeof(TestDataGenerator))]
publicvoidTestWithClassData(int number,string text)
{
Assert.NotNull(text);
}
}
TUnit Equivalent:
publicclassMyTests
{
[Test]
[MethodDataSource(nameof(TestDataGenerator.GetTestData))]
publicasyncTaskTestWithClassData(int number,string text)
{
await Assert.That(text).IsNotNull();
}
}
publicclassTestDataGenerator
{
publicstaticIEnumerable<(int,string)>GetTestData()
{
yieldreturn(1,"one");
yieldreturn(2,"two");
}
}
Key Changes:
[ClassData(typeof(...))]→[MethodDataSource(nameof(ClassName.MethodName))]- Point to a static method rather than implementing IEnumerable
- Use tuples for type safety
Setup and Teardown
Constructor and IDisposable → Before/After Hooks
xUnit Code:
publicclassDatabaseTests:IDisposable
{
privatereadonlyDatabaseConnection _connection;
publicDatabaseTests()
{
_connection =newDatabaseConnection();
_connection.Open();
}
[Fact]
publicvoidQuery_ReturnsData()
{
var result = _connection.Query("SELECT * FROM Users");
Assert.NotNull(result);
}
publicvoidDispose()
{
_connection?.Close();
_connection?.Dispose();
}
}
TUnit Equivalent (Option 1: Using IDisposable):
publicclassDatabaseTests:IDisposable
{
privateDatabaseConnection _connection =null!;
publicDatabaseTests()
{
_connection =newDatabaseConnection();
_connection.Open();
}
[Test]
publicasyncTaskQuery_ReturnsData()
{
var result = _connection.Query("SELECT * FROM Users");
await Assert.That(result).IsNotNull();
}
publicvoidDispose()
{
_connection?.Close();
_connection?.Dispose();
}
}
TUnit Equivalent (Option 2: Using Hooks):
publicclassDatabaseTests
{
privateDatabaseConnection _connection =null!;
[Before(Test)]
publicasyncTaskSetup()
{
_connection =newDatabaseConnection();
await _connection.OpenAsync();
}
[Test]
publicasyncTaskQuery_ReturnsData()
{
var result = _connection.Query("SELECT * FROM Users");
await Assert.That(result).IsNotNull();
}
[After(Test)]
publicasyncTaskCleanup()
{
if(_connection !=null)
{
await _connection.CloseAsync();
_connection.Dispose();
}
}
}
Key Changes:
- Constructor setup can remain, or use
[Before(Test)] - IDisposable can remain, or use
[After(Test)] - Hooks support async operations natively
- Multiple
[After(Test)]methods are guaranteed to run even if one fails
IAsyncLifetime → Before/After Hooks
xUnit Code:
publicclassAsyncSetupTests:IAsyncLifetime
{
privateHttpClient _client =null!;
publicasyncTaskInitializeAsync()
{
_client =newHttpClient();
await _client.GetAsync("https://api.example.com/warm-up");
}
[Fact]
publicasyncTaskFetchData_ReturnsSuccess()
{
var response =await _client.GetAsync("https://api.example.com/data");
Assert.True(response.IsSuccessStatusCode);
}
publicasyncTaskDisposeAsync()
{
_client?.Dispose();
await Task.CompletedTask;
}
}
TUnit Equivalent:
publicclassAsyncSetupTests
{
privateHttpClient _client =null!;
[Before(Test)]
publicasyncTaskSetup()
{
_client =newHttpClient();
await _client.GetAsync("https://api.example.com/warm-up");
}
[Test]
publicasyncTaskFetchData_ReturnsSuccess()
{
var response =await _client.GetAsync("https://api.example.com/data");
await Assert.That(response.IsSuccessStatusCode).IsTrue();
}
[After(Test)]
publicasyncTaskCleanup()
{
_client?.Dispose();
}
}
Key Changes:
IAsyncLifetime.InitializeAsync()→[Before(Test)]IAsyncLifetime.DisposeAsync()→[After(Test)]- More explicit and easier to understand at a glance
Shared Context and Fixtures
IClassFixture → ClassDataSource
xUnit Code:
publicclassDatabaseFixture:IDisposable
{
publicDatabaseConnection Connection {get;}
publicDatabaseFixture()
{
Connection =newDatabaseConnection();
Connection.Open();
}
publicvoidDispose()
{
Connection?.Close();
Connection?.Dispose();
}
}
publicclassUserRepositoryTests:IClassFixture<DatabaseFixture>
{
privatereadonlyDatabaseFixture _fixture;
publicUserRepositoryTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
publicvoidGetUser_ReturnsUser()
{
var repo =newUserRepository(_fixture.Connection);
var user = repo.GetUser(1);
Assert.NotNull(user);
}
[Fact]
publicvoidGetAllUsers_ReturnsUsers()
{
var repo =newUserRepository(_fixture.Connection);
var users = repo.GetAllUsers();
Assert.NotEmpty(users);
}
}
TUnit Equivalent:
publicclassDatabaseFixture:IDisposable
{
publicDatabaseConnection Connection {get;}
publicDatabaseFixture()
{
Connection =newDatabaseConnection();
Connection.Open();
}
publicvoidDispose()
{
Connection?.Close();
Connection?.Dispose();
}
}
[ClassDataSource<DatabaseFixture>(Shared = SharedType.PerClass)]
publicclassUserRepositoryTests(DatabaseFixture fixture)
{
[Test]
publicasyncTaskGetUser_ReturnsUser()
{
var repo =newUserRepository(fixture.Connection);
var user = repo.GetUser(1);
await Assert.That(user).IsNotNull();
}
[Test]
publicasyncTaskGetAllUsers_ReturnsUsers()
{
var repo =newUserRepository(fixture.Connection);
var users = repo.GetAllUsers();
await Assert.That(users).IsNotEmpty();
}
}
Key Changes:
IClassFixture<T>interface →[ClassDataSource<T>(Shared = SharedType.PerClass)]attribute- Fixture injected via primary constructor
Shared = SharedType.PerClassensures one instance per test class
Collection Fixtures → Shared ClassDataSource
xUnit Code:
[CollectionDefinition("Database collection")]
publicclassDatabaseCollection:ICollectionFixture<DatabaseFixture>
{
}
publicclassDatabaseFixture:IDisposable
{
publicDatabaseConnection Connection {get;}
publicDatabaseFixture()
{
Connection =newDatabaseConnection();
Connection.Open();
}
publicvoidDispose()=> Connection?.Dispose();
}
[Collection("Database collection")]
publicclassUserTests:IClassFixture<DatabaseFixture>
{
privatereadonlyDatabaseFixture _fixture;
publicUserTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
publicvoidCreateUser_Succeeds()
{
// Test using _fixture.Connection
}
}
[Collection("Database collection")]
publicclassProductTests:IClassFixture<DatabaseFixture>
{
privatereadonlyDatabaseFixture _fixture;
publicProductTests(DatabaseFixture fixture)
{
_fixture = fixture;
}
[Fact]
publicvoidCreateProduct_Succeeds()
{
// Test using _fixture.Connection
}
}
TUnit Equivalent:
publicclassDatabaseFixture:IDisposable
{
publicDatabaseConnection Connection {get;}
publicDatabaseFixture()
{
Connection =newDatabaseConnection();
Connection.Open();
}
publicvoidDispose()=> Connection?.Dispose();
}
[ClassDataSource<DatabaseFixture>(Shared = SharedType.Keyed, Key ="DatabaseCollection")]
publicclassUserTests(DatabaseFixture fixture)
{
[Test]
publicasyncTaskCreateUser_Succeeds()
{
// Test using fixture.Connection
}
}
[ClassDataSource<DatabaseFixture>(Shared = SharedType.Keyed, Key ="DatabaseCollection")]
publicclassProductTests(DatabaseFixture fixture)
{
[Test]
publicasyncTaskCreateProduct_Succeeds()
{
// Test using fixture.Connection
}
}
Key Changes:
[Collection("name")]→[ClassDataSource<T>(Shared = SharedType.Keyed, Key = "name")]- No need for CollectionDefinition class
- All classes with same Key share the fixture instance
Assembly Fixture → ClassDataSource with PerAssembly
xUnit doesn't have native assembly fixtures, but TUnit does:
TUnit Example:
publicclassApplicationFixture:IDisposable
{
publicIServiceProvider ServiceProvider {get;}
publicApplicationFixture()
{
// Setup once for entire assembly
ServiceProvider =ConfigureServices();
}
publicvoidDispose()
{
// Cleanup once after all tests
}
}
[ClassDataSource<ApplicationFixture>(Shared = SharedType.PerAssembly)]
publicclassIntegrationTests(ApplicationFixture fixture)
{
[Test]
publicasyncTaskTest1()
{
var service = fixture.ServiceProvider.GetService<IMyService>();
await Assert.That(service).IsNotNull();
}
}
Test Output
ITestOutputHelper → TestContext
xUnit Code:
publicclassLoggingTests
{
privatereadonlyITestOutputHelper _output;
publicLoggingTests(ITestOutputHelper output)
{
_output = output;
}
[Fact]
publicvoidTest_WithLogging()
{
_output.WriteLine("Starting test");
var result =PerformOperation();
_output.WriteLine($"Result: {result}");
Assert.True(result >0);
}
}
TUnit Equivalent:
publicclassLoggingTests
{
[Test]
publicasyncTaskTest_WithLogging(TestContext context)
{
context.Output.WriteLine("Starting test");
var result =PerformOperation();
context.Output.WriteLine($"Result: {result}");
await Assert.That(result).IsGreaterThan(0);
}
}
Key Changes:
ITestOutputHelperinjected in constructor →TestContextinjected as method parameter- Access output via
context.Output.WriteLine() - TestContext provides additional test metadata
Test Attachments
xUnit v3 introduced test attachments. TUnit also supports this capability:
xUnit v3 Code:
publicclassTestWithAttachments
{
privatereadonlyITestContextAccessor _testContextAccessor;
publicTestWithAttachments(ITestContextAccessor testContextAccessor)
{
_testContextAccessor = testContextAccessor;
}
[Fact]
publicasyncTaskTest_WithAttachment()
{
// Test logic
var logPath ="test-log.txt";
await File.WriteAllTextAsync(logPath,"test logs");
_testContextAccessor.Current!.Attachments.Add(
newFileAttachment(logPath,"Test Log"));
}
}
TUnit Equivalent:
publicclassTestWithAttachments
{
[Test]
publicasyncTaskTest_WithAttachment()
{
// Test logic
var logPath ="test-log.txt";
await File.WriteAllTextAsync(logPath,"test logs");
TestContext.Current!.Output.AttachArtifact(newArtifact
{
File =newFileInfo(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:
publicclassFeatureTests
{
[Fact]
[Trait("Category","Integration")]
[Trait("Priority","High")]
publicvoidImportantIntegrationTest()
{
// Test implementation
}
}
TUnit Equivalent:
publicclassFeatureTests
{
[Test]
[Property("Category","Integration")]
[Property("Priority","High")]
publicasyncTaskImportantIntegrationTest()
{
// Test implementation
}
}
Key Changes:
[Trait("key", "value")]→[Property("key", "value")]- Can be used for filtering:
--treenode-filter "/*/*/*/*[Category=Integration]"
Assertions
Basic Assertions
xUnit Code:
[Fact]
publicvoidAssertions_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]
publicasyncTaskAssertions_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]
publicvoidCollection_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]
publicasyncTaskCollection_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]
publicvoidString_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]
publicasyncTaskString_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]
publicvoidException_Assertions()
{
Assert.Throws<ArgumentException>(()=>ThrowsException());
var ex = Assert.Throws<ArgumentException>(()=>ThrowsException());
Assert.Equal("paramName", ex.ParamName);
}
[Fact]
publicasyncTaskAsync_Exception_Assertions()
{
await Assert.ThrowsAsync<InvalidOperationException>(()=>ThrowsExceptionAsync());
}
TUnit Equivalent:
[Test]
publicasyncTaskException_Assertions()
{
Assert.Throws<ArgumentException>(()=>ThrowsException());
var ex = Assert.Throws<ArgumentException>(()=>ThrowsException());
await Assert.That(ex.ParamName).IsEqualTo("paramName");
}
[Test]
publicasyncTaskAsync_Exception_Assertions()
{
await Assert.ThrowsAsync<InvalidOperationException>(()=>ThrowsExceptionAsync());
}
Key Changes:
- Sync exception assertions use
Assert.Throws - Async exception assertions use
Assert.ThrowsAsync - Returned exception can be further asserted on
Complete Example: Real-World Test Class
xUnit Code:
publicclassUserServiceTests:IClassFixture<DatabaseFixture>,IAsyncLifetime
{
privatereadonlyDatabaseFixture _dbFixture;
privatereadonlyITestOutputHelper _output;
privateUserService _userService =null!;
publicUserServiceTests(DatabaseFixture dbFixture,ITestOutputHelper output)
{
_dbFixture = dbFixture;
_output = output;
}
publicasyncTaskInitializeAsync()
{
_userService =newUserService(_dbFixture.Connection);
await _userService.InitializeAsync();
}
publicTaskDisposeAsync()=> Task.CompletedTask;
[Theory]
[InlineData("john@example.com","John")]
[InlineData("jane@example.com","Jane")]
publicasyncTaskCreateUser_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]
publicasyncTaskGetUser_WhenNotFound_ThrowsException()
{
await Assert.ThrowsAsync<UserNotFoundException>(
()=> _userService.GetUserAsync(99999));
}
[Theory]
[MemberData(nameof(GetInvalidEmails))]
publicasyncTaskCreateUser_WithInvalidEmail_ThrowsException(string invalidEmail)
{
await Assert.ThrowsAsync<ArgumentException>(
()=> _userService.CreateUserAsync(invalidEmail,"Test"));
}
publicstaticIEnumerable<object[]>GetInvalidEmails()
{
yieldreturnnewobject[]{""};
yieldreturnnewobject[]{"not-an-email"};
yieldreturnnewobject[]{"@example.com"};
}
}
TUnit Equivalent:
[ClassDataSource<DatabaseFixture>(Shared = SharedType.PerClass)]
publicclassUserServiceTests(DatabaseFixture dbFixture)
{
privateUserService _userService =null!;
[Before(Test)]
publicasyncTaskSetup()
{
_userService =newUserService(dbFixture.Connection);
await _userService.InitializeAsync();
}
[Test]
[Arguments("john@example.com","John")]
[Arguments("jane@example.com","Jane")]
publicasyncTaskCreateUser_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]
publicasyncTaskGetUser_WhenNotFound_ThrowsException()
{
await Assert.ThrowsAsync<UserNotFoundException>(
()=> _userService.GetUserAsync(99999));
}
[Test]
[MethodDataSource(nameof(GetInvalidEmails))]
publicasyncTaskCreateUser_WithInvalidEmail_ThrowsException(string invalidEmail)
{
await Assert.ThrowsAsync<ArgumentException>(
()=> _userService.CreateUserAsync(invalidEmail,"Test"));
}
publicstaticIEnumerable<string>GetInvalidEmails()
{
yieldreturn"";
yieldreturn"not-an-email";
yieldreturn"@example.com";
}
}
Key Differences Summary:
- Class-level fixtures use attributes instead of interfaces
- Setup/teardown use
[Before]/[After]attributes instead of IAsyncLifetime - Primary constructor for fixture injection
- TestContext injected as method parameter when needed
- All tests are async by default
- Data sources return strongly-typed values (not object[])
- Fluent assertion syntax
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.
