Migrating from NUnit to TUnit can improve test execution speed. Check the benchmarks to see how TUnit compares.
Quick Reference
| NUnit | TUnit |
|---|---|
[TestFixture] | (remove - not needed) |
[Test] | [Test] |
[TestCase(...)] | [Arguments(...)] |
[TestCaseSource(nameof(...))] | [MethodDataSource(nameof(...))] |
[Category("value")] | [Category("value")] (same) or [Property("Category", "value")] |
[Ignore] | [Skip] |
[Explicit] | [Explicit] |
[SetUp] | [Before(Test)] |
[TearDown] | [After(Test)] |
[OneTimeSetUp] | [Before(Class)] |
[OneTimeTearDown] | [After(Class)] |
[SetUpFixture] + [OneTimeSetUp] | [Before(Assembly)] on static method |
[Values(...)] on parameter | [Matrix(...)] on each parameter |
Assert.AreEqual(expected, actual) | await Assert.That(actual).IsEqualTo(expected) |
Assert.That(actual, Is.EqualTo(expected)) | await Assert.That(actual).IsEqualTo(expected) |
Assert.Throws<T>(() => ...) | await Assert.ThrowsAsync<T>(() => ...) |
TestContext.WriteLine(...) | TestContext parameter with context.Output.WriteLine(...) |
TestContext.AddTestAttachment(path, name) | TestContext.Current!.Output.AttachArtifact(new Artifact { File = new FileInfo(path), DisplayName = name }) |
CollectionAssert.AreEqual(expected, actual) | await Assert.That(actual).IsEquivalentTo(expected) |
StringAssert.Contains(substring, text) | await Assert.That(text).Contains(substring) |
Automated Migration with Code Fixers
TUnit includes Roslyn analyzers and code fixers that automate most of the migration work. The TUNU0001 diagnostic identifies NUnit code patterns and provides automatic fixes to convert them to TUnit equivalents.
What gets converted automatically:
[TestFixture]→ removed (not needed in TUnit)[Test]→[Test](stays the same)[TestCase(...)]→[Arguments(...)][TestCaseSource(nameof(...))]→[MethodDataSource(nameof(...))][SetUp]→[Before(Test)][TearDown]→[After(Test)][OneTimeSetUp]→[Before(Class)][OneTimeTearDown]→[After(Class)][Ignore]→[Skip][Category("...")]→[Property("Category", "...")]- Classic assertions:
Assert.AreEqual(expected, actual)→await Assert.That(actual).IsEqualTo(expected) - Constraint assertions:
Assert.That(actual, Is.EqualTo(expected))→await Assert.That(actual).IsEqualTo(expected) - Test methods converted to
async Taskwithawaiton assertions
The code fixer handles roughly 80-90% of typical test suites automatically.
What requires manual adjustment:
- Custom
[TestCaseSource]return types (convertobject[]to tuples) - Complex async patterns or custom awaitable types
- Custom fixtures or test base classes
[SetUpFixture]with namespace-scoped setup (convert to assembly hooks)TestContext.CurrentContextstatic access (injectTestContextas parameter instead)Assert.Multipleblocks (use assertion groups or multiple awaited assertions)
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 NUnit (temporarily):
dotnet add package TUnit
2. Disable TUnit's implicit usings (temporary)
Add these properties to your .csproj to prevent type name conflicts between NUnit and TUnit:
<PropertyGroup>
<TUnitImplicitUsings>false</TUnitImplicitUsings>
<TUnitAssertionsImplicitUsings>false</TUnitAssertionsImplicitUsings>
</PropertyGroup>
This allows the code fixer to distinguish between NUnit.Framework.Assert and TUnit.Assertions.Assert.
3. Rebuild the project
dotnet build
This restores packages and loads the TUnit analyzers.
The TUNU0001 diagnostic is information-level and won't appear in standard build output. If you want to verify the analyzer is detecting NUnit code before applying changes, run:
dotnet format analyzers --severity info --diagnostics TUNU0001 --verify-no-changes
This command checks for TUNU0001 diagnostics without modifying any files. If NUnit 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 TUNU0001
This command applies all available fixes for the TUNU0001 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 TUNU0001 --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:
- Add
using TUnit.Core;andusing TUnit.Assertions;if not using implicit usings - Convert data source methods to return tuples instead of
object[] - Replace
TestContext.CurrentContextwith injectedTestContextparameter - Update any custom assertion extensions
7. Remove NUnit packages
Once everything compiles and tests pass:
dotnet remove package NUnit
dotnet remove package NUnit3TestAdapter
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
TUNU0001warnings appear in build output - Try running with verbose output:
dotnet format analyzers --severity info --diagnostics TUNU0001 --verbosity detailed
Build errors after running code fixer:
- Missing
awaitkeywords: ensure test methods areasync Task - Ambiguous
Assert: remove NUnit usings or fully qualify types - Type mismatch in data sources: convert
object[]returns to tuples
Analyzers not loading:
- Verify TUnit package is installed:
dotnet list package - Try cleaning and rebuilding:
dotnet clean && dotnet build
Manual Migration Guide
Test Attributes
[TestFixture] - Remove this attribute (not needed in TUnit)
[Test] remains [Test]
[TestCase] becomes [Arguments]
[TestCaseSource] becomes [MethodDataSource]
[Category] becomes [Property("Category", "value")]
[Ignore] becomes [Skip]
[Explicit] remains [Explicit]
Setup and Teardown
[SetUp] becomes [Before(Test)]
[TearDown] becomes [After(Test)]
[OneTimeSetUp] becomes [Before(Class)]
[OneTimeTearDown] becomes [After(Class)]
Assertions
Classic Assertions
// NUnit
Assert.AreEqual(expected, actual);
Assert.IsTrue(condition);
Assert.IsNull(value);
Assert.Greater(value1, value2);
// TUnit
await Assert.That(actual).IsEqualTo(expected);
await Assert.That(condition).IsTrue();
await Assert.That(value).IsNull();
await Assert.That(value1).IsGreaterThan(value2);
Constraint-Based Assertions
// NUnit
Assert.That(actual, Is.EqualTo(expected));
Assert.That(value, Is.True);
Assert.That(value, Is.Null);
Assert.That(text, Does.Contain("substring"));
Assert.That(collection, Has.Count.EqualTo(5));
// TUnit
await Assert.That(actual).IsEqualTo(expected);
await Assert.That(value).IsTrue();
await Assert.That(value).IsNull();
await Assert.That(text).Contains("substring");
await Assert.That(collection).Count().IsEqualTo(5);
Collection Assertions
// NUnit
CollectionAssert.AreEqual(expected, actual);
CollectionAssert.Contains(collection, item);
CollectionAssert.IsEmpty(collection);
// TUnit
await Assert.That(actual).IsEquivalentTo(expected);
await Assert.That(collection).Contains(item);
await Assert.That(collection).IsEmpty();
String Assertions
// NUnit
StringAssert.Contains(substring, text);
StringAssert.StartsWith(prefix, text);
StringAssert.EndsWith(suffix, text);
// TUnit
await Assert.That(text).Contains(substring);
await Assert.That(text).StartsWith(prefix);
await Assert.That(text).EndsWith(suffix);
Exception Testing
// NUnit
Assert.Throws<InvalidOperationException>(()=>DoSomething());
Assert.ThrowsAsync<InvalidOperationException>(async()=>awaitDoSomethingAsync());
// TUnit
await Assert.ThrowsAsync<InvalidOperationException>(()=>DoSomething());
await Assert.ThrowsAsync<InvalidOperationException>(async()=>awaitDoSomethingAsync());
Test Data Sources
TestCaseSource
// NUnit
[TestCaseSource(nameof(TestData))]
publicvoidTestMethod(intvalue,string text)
{
// Test implementation
}
privatestaticIEnumerableTestData()
{
yieldreturnnewobject[]{1,"one"};
yieldreturnnewobject[]{2,"two"};
}
// TUnit
[MethodDataSource(nameof(TestData))]
publicasyncTaskTestMethod(intvalue,string text)
{
// Test implementation
}
privatestaticIEnumerable<(int,string)>TestData()
{
yieldreturn(1,"one");
yieldreturn(2,"two");
}
Parameterized Tests
// NUnit
[TestCase(1,2,3)]
[TestCase(10,20,30)]
publicvoidAdditionTest(int a,int b,int expected)
{
Assert.AreEqual(expected, a + b);
}
// TUnit
[Test]
[Arguments(1,2,3)]
[Arguments(10,20,30)]
publicasyncTaskAdditionTest(int a,int b,int expected)
{
await Assert.That(a + b).IsEqualTo(expected);
}
Test Output
// NUnit
TestContext.WriteLine("Test output");
TestContext.Out.WriteLine("More output");
// TUnit (inject TestContext)
publicasyncTaskMyTest(TestContext context)
{
context.Output.WriteLine("Test output");
context.Output.WriteLine("More output");
}
Test Attachments
// NUnit
[Test]
publicvoidTestWithAttachment()
{
// Test logic
var logPath ="test-log.txt";
File.WriteAllText(logPath,"test logs");
TestContext.AddTestAttachment(logPath,"Test Log");
}
// TUnit
[Test]
publicasyncTaskTestWithAttachment()
{
// 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.
Combinatorial Testing
Values and Combinatorial → Matrix
NUnit Code:
publicclassCombinationTests
{
[Test]
publicvoidTestCombinations(
[Values(1,2,3)]int x,
[Values("a","b")]string y)
{
Assert.That(x, Is.GreaterThan(0));
Assert.That(y, Is.Not.Null);
}
}
TUnit Equivalent:
publicclassCombinationTests
{
[Test]
publicasyncTaskTestCombinations(
[Matrix(1,2,3)]int x,
[Matrix("a","b")]string y)
{
await Assert.That(x).IsGreaterThan(0);
await Assert.That(y).IsNotNull();
}
}
Key Changes:
[Values(...)]attributes on parameters →[Matrix(...)]attributes on parameters- All combinations are automatically generated (3 × 2 = 6 test cases)
- Each parameter gets its own
[Matrix]attribute with the values to test
Test Fixture with Parameters
Parameterized TestFixture
NUnit Code:
[TestFixture("Development")]
[TestFixture("Staging")]
[TestFixture("Production")]
publicclassEnvironmentTests
{
privatereadonlystring _environment;
publicEnvironmentTests(string environment)
{
_environment = environment;
}
[Test]
publicvoidConfigurationIsValid()
{
var config =LoadConfiguration(_environment);
Assert.That(config, Is.Not.Null);
Assert.That(config.IsValid, Is.True);
}
}
TUnit Equivalent:
[Arguments("Development")]
[Arguments("Staging")]
[Arguments("Production")]
publicclassEnvironmentTests(string environment)
{
[Test]
publicasyncTaskConfigurationIsValid()
{
var config =LoadConfiguration(environment);
await Assert.That(config).IsNotNull();
await Assert.That(config.IsValid).IsTrue();
}
}
Key Changes:
[TestFixture(...)]with parameters →[Arguments(...)]on the class- Primary constructor for cleaner syntax
- All tests in the class are repeated for each argument set
Complete Test Class Example
NUnit Code:
[TestFixture]
publicclassProductServiceTests
{
privateIDatabase _database;
privateProductService _productService;
[OneTimeSetUp]
publicvoidOneTimeSetup()
{
// Runs once before all tests in the class
_database =newInMemoryDatabase();
_database.Initialize();
}
[SetUp]
publicvoidSetup()
{
// Runs before each test
_productService =newProductService(_database);
}
[Test]
[Category("Unit")]
[TestCase("Widget",10.99)]
[TestCase("Gadget",25.50)]
publicvoidCreateProduct_WithValidData_Succeeds(string name,decimal price)
{
var product = _productService.CreateProduct(name, price);
Assert.That(product, Is.Not.Null);
Assert.That(product.Name, Is.EqualTo(name));
Assert.That(product.Price, Is.EqualTo(price));
}
[Test]
[Category("Unit")]
publicvoidGetProduct_WhenNotFound_ReturnsNull()
{
var product = _productService.GetProduct(999);
Assert.That(product, Is.Null);
}
[Test]
[TestCaseSource(nameof(InvalidProductData))]
publicvoidCreateProduct_WithInvalidData_ThrowsException(string name,decimal price)
{
Assert.Throws<ArgumentException>(()=> _productService.CreateProduct(name, price));
}
privatestaticIEnumerableInvalidProductData()
{
yieldreturnnewobject[]{"",10.00};
yieldreturnnewobject[]{"Product",-5.00};
yieldreturnnewobject[]{null,10.00};
}
[TearDown]
publicvoidTearDown()
{
// Runs after each test
_productService?.Dispose();
}
[OneTimeTearDown]
publicvoidOneTimeTearDown()
{
// Runs once after all tests in the class
_database?.Dispose();
}
}
TUnit Equivalent:
publicclassProductServiceTests
{
privateIDatabase _database =null!;
privateProductService _productService =null!;
[Before(Class)]
publicasyncTaskClassSetup()
{
// Runs once before all tests in the class
_database =newInMemoryDatabase();
await _database.InitializeAsync();
}
[Before(Test)]
publicasyncTaskSetup()
{
// Runs before each test
_productService =newProductService(_database);
}
[Test]
[Property("Category","Unit")]
[Arguments("Widget",10.99)]
[Arguments("Gadget",25.50)]
publicasyncTaskCreateProduct_WithValidData_Succeeds(string name,decimal price)
{
var product = _productService.CreateProduct(name, price);
await Assert.That(product).IsNotNull();
await Assert.That(product.Name).IsEqualTo(name);
await Assert.That(product.Price).IsEqualTo(price);
}
[Test]
[Property("Category","Unit")]
publicasyncTaskGetProduct_WhenNotFound_ReturnsNull()
{
var product = _productService.GetProduct(999);
await Assert.That(product).IsNull();
}
[Test]
[MethodDataSource(nameof(InvalidProductData))]
publicasyncTaskCreateProduct_WithInvalidData_ThrowsException(string name,decimal price)
{
await Assert.ThrowsAsync<ArgumentException>(
()=> _productService.CreateProduct(name, price));
}
privatestaticIEnumerable<(string name,decimal price)>InvalidProductData()
{
yieldreturn("",10.00m);
yieldreturn("Product",-5.00m);
yieldreturn(null!,10.00m);
}
[After(Test)]
publicasyncTaskCleanup()
{
// Runs after each test
_productService?.Dispose();
}
[After(Class)]
publicasyncTaskClassCleanup()
{
// Runs once after all tests in the class
_database?.Dispose();
}
}
Key Changes:
[TestFixture]attribute removed (not needed)[OneTimeSetUp]→[Before(Class)](and can be async)[SetUp]→[Before(Test)][TearDown]→[After(Test)][OneTimeTearDown]→[After(Class)][TestCase(...)]→[Arguments(...)]- Data sources return tuples instead of
object[] - All assertions are awaited
Range Testing
NUnit Code:
[Test]
publicvoidProcessValue_WithRange([Range(1,10)]intvalue)
{
var result =ProcessValue(value);
Assert.That(result, Is.GreaterThan(0));
}
TUnit Equivalent:
[Test]
[MethodDataSource(nameof(GetRange))]
publicasyncTaskProcessValue_WithRange(intvalue)
{
var result =ProcessValue(value);
await Assert.That(result).IsGreaterThan(0);
}
privatestaticIEnumerable<int>GetRange()
{
return Enumerable.Range(1,10);
}
Custom Test Context Properties
NUnit Code:
[Test]
publicvoidTest_WithContextProperties()
{
TestContext.WriteLine($"Test Name: {TestContext.CurrentContext.Test.Name}");
TestContext.WriteLine($"Test Status: {TestContext.CurrentContext.Result.Outcome.Status}");
// Test implementation
}
TUnit Equivalent:
[Test]
publicasyncTaskTest_WithContextProperties(TestContext context)
{
context.Output.WriteLine($"Test Name: {context.Metadata.TestName}");
context.Output.WriteLine($"Test ID: {context.Metadata.TestDetails.TestId}");
context.Output.WriteLine($"Class Name: {context.Metadata.TestDetails.ClassType.Name}");
// Test implementation
}
Assertion Constraint Mapping
NUnit Code:
[Test]
publicvoidComplexAssertions()
{
varvalue=42;
var text ="Hello World";
var list =new[]{1,2,3,4,5};
// Comparison assertions
Assert.That(value, Is.EqualTo(42));
Assert.That(value, Is.Not.EqualTo(0));
Assert.That(value, Is.GreaterThan(40));
Assert.That(value, Is.LessThanOrEqualTo(50));
Assert.That(value, Is.InRange(40,45));
// String assertions
Assert.That(text, Does.StartWith("Hello"));
Assert.That(text, Does.EndWith("World"));
Assert.That(text, Does.Contain("llo Wor"));
Assert.That(text, Does.Match(@"^Hello"));
// Collection assertions
Assert.That(list, Has.Count.EqualTo(5));
Assert.That(list, Has.Member(3));
Assert.That(list, Has.All.GreaterThan(0));
Assert.That(list, Is.Ordered);
// Compound assertions
Assert.That(value, Is.GreaterThan(40).And.LessThan(50));
Assert.That(text, Is.Not.Null.And.Not.Empty);
}
TUnit Equivalent:
[Test]
publicasyncTaskComplexAssertions()
{
varvalue=42;
var text ="Hello World";
var list =new[]{1,2,3,4,5};
// Comparison assertions
await Assert.That(value).IsEqualTo(42);
await Assert.That(value).IsNotEqualTo(0);
await Assert.That(value).IsGreaterThan(40);
await Assert.That(value).IsLessThanOrEqualTo(50);
await Assert.That(value).IsBetween(40,45);
// String assertions
await Assert.That(text).StartsWith("Hello");
await Assert.That(text).EndsWith("World");
await Assert.That(text).Contains("llo Wor");
await Assert.That(text).Matches(@"^Hello");
// Collection assertions
await Assert.That(list).Count().IsEqualTo(5);
await Assert.That(list).Contains(3);
await Assert.That(list).All().Satisfy(item => item.IsGreaterThan(0));
await Assert.That(list).IsInOrder();
// Compound assertions (using And/Or)
await Assert.That(value).IsGreaterThan(40).And.IsLessThan(50);
await Assert.That(text).IsNotNull().And.IsNotEmpty();
}
SetUpFixture for Assembly-Level Hooks
NUnit Code:
[SetUpFixture]
publicclassAssemblySetup
{
[OneTimeSetUp]
publicvoidRunBeforeAnyTests()
{
// Initialize resources needed by all tests
Console.WriteLine("Assembly setup running");
}
[OneTimeTearDown]
publicvoidRunAfterAllTests()
{
// Cleanup resources
Console.WriteLine("Assembly cleanup running");
}
}
TUnit Equivalent:
publicstaticclassAssemblyHooks
{
[Before(Assembly)]
publicstaticasyncTaskAssemblySetup()
{
// Initialize resources needed by all tests
Console.WriteLine("Assembly setup running");
}
[After(Assembly)]
publicstaticasyncTaskAssemblyCleanup()
{
// Cleanup resources
Console.WriteLine("Assembly cleanup running");
}
}
Key Changes:
[SetUpFixture]→ simple static class[OneTimeSetUp]→[Before(Assembly)][OneTimeTearDown]→[After(Assembly)]- Methods must be static
- Can be async
Key Differences to Note
-
Async by Default: TUnit tests and assertions are async by default. Add
async Taskto your test methods andawaitassertions. -
No TestFixture Required: TUnit doesn't require a
[TestFixture]attribute on test classes. -
Fluent Assertions: TUnit uses a fluent assertion style with
Assert.That()as the starting point. -
Dependency Injection: TUnit has built-in support for dependency injection in test classes and methods.
-
Hooks Instead of Setup/Teardown: TUnit uses
[Before]and[After]attributes withHookTypeto specify when they run. -
TestContext Injection: Instead of a static
TestContext, TUnit injects it as a parameter where needed. -
Isolated Test Instances: Each test runs in its own class instance (NUnit's default behavior can be different).
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.
