| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
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);
}If using TUnit Assertions, 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)
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.57 ms | 281.32 ms | 655.45 ms | 564.15 ms | 507.18 ms |
| Async-heavy tests | 116.0 ms | 388.5 ms | 730.9 ms | 714.3 ms | 664.8 ms |
| Matrix combinations | 117.1 ms | 364.9 ms | 861.9 ms | 1,536.5 ms | 1,497.2 ms |
| Large suites (scale) | 18.88 ms | 334.54 ms | 710.91 ms | 643.81 ms | 562.39 ms |
| Massive parallelism | 220.9 ms | 535.8 ms | 1,337.9 ms | 1,317.5 ms | 3,040.5 ms |
| Setup/teardown lifecycle | 75.64 ms | 367.81 ms | 955.39 ms | 1,270.05 ms | 1,322.57 ms |
Mean wall-clock time to run the same test suite. TUnit (AOT) 1.65.68 · TUnit 1.65.68 · xUnit v3 4.0.0 · NUnit 4.6.1 · MSTest 4.3.3. .NET SDK 10.0.400, .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4. Updated 2026-08-30 — 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:
var account = new { Balance = 1 };
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() => Task.CompletedTask; // start container
public ValueTask DisposeAsync() => ValueTask.CompletedTask; // 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(TestRegisteredContext 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.
Solutions and repository-wide SDK, package, and build configuration remain at root.
| Back | FazBrowse Home | New Git URL |