A modern .NET testing framework. Tests are discovered at compile time via source generators, run in parallel by default, and work under Native AOT — all built on Microsoft.Testing.Platform.
[Test]
[Arguments("GOLD", 100.00, 80.00)]
[Arguments("SILVER", 100.00, 90.00)]
public async Task Discount_Is_Applied(string tier, double subtotal, double expected)
{
var checkout = new CheckoutService();
var total = await checkout.ApplyDiscountAsync(tier, subtotal);
await Assert.That(total).IsEqualTo(expected);
}When a test fails, TUnit tells you what happened — including the actual expression you wrote:
Expected to be 80
but found 100
at Assert.That(total).IsEqualTo(expected)
Comparing objects? Instead of dumping two object graphs at you, TUnit pinpoints the difference:
Expected to be equal to Employee { FirstName = "Victoria", LastName = "Apanii", Age = 30 }
but differs at member FirstName: expected "Victoria" but found "ictoria"
at Assert.That(actualEmployee).IsEqualTo(expectedEmployee)
- Compile-time test discovery — tests are wired up by a source generator at build time, not found via reflection at runtime. Faster startup, better IDE integration, and full Native AOT / trimming support.
- Compile-time safety — a suite of Roslyn analyzers ships in the box, so mistakes like invalid hook signatures, broken data sources, and misused assertions fail your build, not your CI run.
- Parallel by default, with real control — tests run concurrently out of the box;
[DependsOn],[NotInParallel], and[ParallelLimiter<T>]give you precise ordering and throttling when you need it. - Batteries included — rich async assertions, shared fixtures with dependency injection, lifecycle hooks at every scope, and a source-generated mocking library — with first-class integrations for ASP.NET Core, Aspire, and Playwright.
Source generation shifts work from run time to build time: you pay a little up front at build, and every test run after that starts faster — dramatically so under Native AOT. The same test suites, run on every framework:
| Scenario | TUnit (AOT) | TUnit | xUnit v3 | NUnit | MSTest |
|---|---|---|---|---|---|
| Data-driven tests | 16.65 ms | 265.70 ms | 523.90 ms | 523.01 ms | 529.13 ms |
| Async-heavy tests | 113.1 ms | 360.0 ms | 605.6 ms | 608.6 ms | 676.0 ms |
| Matrix combinations | 119.6 ms | 474.1 ms | 1,469.2 ms | 1,468.9 ms | 1,515.3 ms |
| Large suites (scale) | 29.38 ms | 263.55 ms | 504.14 ms | 488.30 ms | 495.94 ms |
| Massive parallelism | 217.6 ms | 467.8 ms | 2,945.7 ms | 1,107.5 ms | 2,993.9 ms |
| Setup/teardown lifecycle | — | 335.8 ms | 1,050.2 ms | 1,008.8 ms | 1,099.9 ms |
Mean wall-clock time to run the same test suite. TUnit (AOT) 1.58.0 · TUnit 1.58.0 · xUnit v3 3.2.2 · NUnit 4.6.1 · MSTest 4.3.0. .NET SDK 10.0.301, .NET 10.0.9 (10.0.9, 10.0.926.27113), X64 RyuJIT x86-64-v4. Updated 2026-07-12 — regenerated weekly by the Speed Comparison workflow. Full results and methodology: tunit.dev/docs/benchmarks.
dotnet new install TUnit.Templates
dotnet new TUnit -n "MyTestProject"
cd MyTestProject
dotnet rundotnet add package TUnitGetting Started Guide · Migration Guides
[Test]
[Arguments("user1@test.com", "ValidPassword123")]
[Arguments("admin@test.com", "AdminPass789")]
public async Task User_Login_Succeeds(string email, string password) { ... }
// Matrix — generates a test for every combination (9 total here)
[Test]
[MatrixDataSource]
public async Task Database_Operations_Work(
[Matrix("Create", "Update", "Delete")] string operation,
[Matrix("User", "Product", "Order")] string entity) { ... }Need more? [MethodDataSource] pulls rows from a method, and custom DataSourceGenerator<T> attributes let you build your own sources.
Assertions are async, chainable, and produce the focused failure messages shown above:
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK)
.Because("the health endpoint should always be up");
await Assert.That(order.Items)
.Count().IsEqualTo(3)
.And.Contains(item => item.Sku == "ABC-123");Defining your own assertion is one attribute on a plain method — TUnit generates the fluent extension for you:
[GenerateAssertion]
public static bool IsPositive(this int value) => value > 0;
// Now available on Assert.That:
await Assert.That(account.Balance).IsPositive();Inject anything into your test classes with [ClassDataSource<T>]. Implement IAsyncInitializer for async setup, IAsyncDisposable for teardown, and pick a sharing scope — None, PerClass, PerAssembly, PerTestSession, or Keyed:
public class PostgresContainer : IAsyncInitializer, IAsyncDisposable
{
public Task InitializeAsync() { /* start container */ }
public ValueTask DisposeAsync() { /* stop container */ }
}
public class OrderRepositoryTests
{
[ClassDataSource<PostgresContainer>(Shared = SharedType.PerTestSession)]
public required PostgresContainer Postgres { get; init; }
[Test]
public async Task Saves_Order() { /* Postgres is initialized and shared across the whole run */ }
}Property injection keeps base test classes clean — subclasses inherit the fixture without re-threading constructor parameters. Prefer a constructor param? That works too. Disposal is reference-counted, so shared fixtures are torn down exactly when the last test using them finishes.
Everything runs in parallel by default. Opt out or sequence tests where it matters:
[Test]
public async Task Register_User() { ... }
[Test, DependsOn(nameof(Register_User))]
[Retry(3)]
public async Task Login_With_Registered_User() { ... } // runs after Register_User passes
[Test, NotInParallel("checkout-db")] // tests sharing a key never overlap
public async Task Migrates_Schema() { ... }[Repeat(n)], [Timeout(ms)], and [ParallelLimiter<T>] round out the set.
[Before(Test)] // also: Class, Assembly, TestSession
public async Task SetUp() { ... }
[After(Class)]
public static async Task TearDownDatabase(ClassHookContext context) { ... }TUnit.Mocks is a source-generated, Native AOT-compatible mocking library — no runtime proxies, no Castle.Core. It works with any test framework:
var gateway = IPaymentGateway.Mock(); // or Mock.Of<IPaymentGateway>()
gateway.ChargeAsync(Any<decimal>()).Returns(new ChargeResult(Success: true));
var checkout = new CheckoutService(gateway.Object);
await checkout.CompleteAsync(cart);
gateway.ChargeAsync(99.99m).WasCalled(Times.Once);Companion packages mock the annoying stuff for you:
// TUnit.Mocks.Http — a real HttpClient backed by a scriptable handler
using var client = Mock.HttpClient("https://api.example.com");
client.Handler.OnGet("/users/1").RespondWithJson("""{ "id": 1 }""");
// TUnit.Mocks.Logging — capture and verify ILogger output
var logger = Mock.Logger<CheckoutService>();
logger.VerifyLog().AtLevel(LogLevel.Warning).ContainingMessage("retrying").WasCalled(Times.Once);Extend built-in base classes to create your own skip conditions, retry logic, and more:
public class WindowsOnlyAttribute : SkipAttribute
{
public WindowsOnlyAttribute() : base("Windows only") { }
public override Task<bool> ShouldSkip(TestContext testContext)
=> Task.FromResult(!OperatingSystem.IsWindows());
}
[Test, WindowsOnly]
public async Task Windows_Specific_Feature() { ... }public class ApiFactory : TestWebApplicationFactory<Program>;
[ClassDataSource<ApiFactory>(Shared = SharedType.PerTestSession)]
public class HealthCheckTests(ApiFactory factory)
{
[Test]
public async Task Health_Endpoint_Responds()
{
using var client = factory.CreateClient();
var response = await client.GetAsync("/health");
await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK);
}
}Spin up your whole distributed app once per test session, with resource log forwarding and OpenTelemetry capture built in:
public class AppFixture : AspireFixture<Projects.MyApp_AppHost>;
[ClassDataSource<AppFixture>(Shared = SharedType.PerTestSession)]
public class ApiServiceTests(AppFixture app)
{
[Test]
public async Task Api_Returns_Data()
{
var client = app.CreateHttpClient("apiservice");
var response = await client.GetAsync("/weather");
await Assert.That(response.IsSuccessStatusCode).IsTrue();
}
}Inherit from PageTest and a browser page is waiting for you — lifecycle fully managed:
public class HomePageTests : PageTest
{
[Test]
public async Task Homepage_Loads()
{
await Page.GotoAsync("https://example.com");
await Assert.That(await Page.TitleAsync()).Contains("Example");
}
}[Test, FsCheckProperty]
public bool Reversing_Twice_Returns_Original(int[] array) =>
array.SequenceEqual(array.AsEnumerable().Reverse().Reverse());TUnit runs F# and VB.NET test projects too, and TUnit.Assertions.FSharp provides idiomatic F# assertion helpers.
| IDE | Notes |
|---|---|
| Visual Studio 2022 (17.13+) | Works out of the box |
| Visual Studio 2022 (earlier) | Enable "Use testing platform server mode" in Tools > Manage Preview Features |
| JetBrains Rider | Enable "Testing Platform support" in Settings > Build, Execution, Deployment > Unit Testing > Testing Platform |
| VS Code | Install C# Dev Kit and enable "Use Testing Platform Protocol" |
| CLI | Works with dotnet test, dotnet run, and direct execution |
| Package | Purpose |
|---|---|
TUnit |
Start here — the full framework (Core + Engine + Assertions) |
TUnit.Core |
Shared test library components without an execution engine |
TUnit.Engine |
Execution engine for test projects |
TUnit.Assertions |
Standalone assertions — works with other test frameworks too |
TUnit.Assertions.Should |
Optional FluentAssertions-style value.Should().BeEqualTo(...) syntax over TUnit.Assertions (beta) |
TUnit.Mocks |
Source-generated, AOT-compatible mocking — works with any test runner |
TUnit.Mocks.Http |
HttpClient mocking helpers built on TUnit.Mocks |
TUnit.Mocks.Logging |
ILogger capture/verification helpers built on TUnit.Mocks |
TUnit.AspNetCore |
ASP.NET Core integration — WebApplicationFactory-based test fixtures |
TUnit.Aspire |
Aspire integration — distributed app host fixtures with OpenTelemetry capture |
TUnit.Playwright |
Playwright integration with automatic browser lifecycle management |
TUnit.FsCheck |
Property-based testing via FsCheck |
TUnit.OpenTelemetry |
OpenTelemetry instrumentation for test runs |
The syntax will feel familiar. For example, xUnit's [Fact] becomes [Test], and [Theory] + [InlineData] becomes [Test] + [Arguments]. See the migration guides for full details: xUnit · NUnit · MSTest.
- Documentation — guides, tutorials, and API reference
- GitHub Discussions — questions and ideas welcome
- Issues — bug reports and feature requests
- Changelog
