# TUnit - Modern Testing Framework for .NET ## Introduction TUnit is a next-generation testing framework for .NET that revolutionizes test execution through compile-time test discovery using source generators instead of runtime reflection. Built on Microsoft.Testing.Platform, TUnit prioritizes performance, parallelism, and modern .NET capabilities. The framework discovers tests at build time through incremental source generators, creating TestMetadata structures that are expanded into executable tests at runtime by the TestBuilder. This dual-mode architecture (source-generated and reflection fallback) ensures both maximum performance and flexibility, while maintaining full compatibility with Native AOT compilation and assembly trimming. TUnit distinguishes itself from traditional testing frameworks (xUnit, NUnit, MSTest) through several core innovations: tests run in parallel by default with sophisticated dependency management via `[DependsOn]` attributes, the framework is fully AOT-compatible with no dynamic reflection incompatibilities, and it provides built-in Roslyn analyzers for compile-time validation. The architecture separates compile-time concerns (source generators emit only data structures) from runtime concerns (execution engine handles complex logic), resulting in faster test discovery, better IDE integration, predictable resource management, and improved debugging experiences. The framework supports .NET Standard 2.0 for library compatibility and .NET 6, 8, 9+ for active development. ## Core APIs and Functions ### Basic Test Definition ```csharp using TUnit.Core; using TUnit.Assertions; // Simple test - discovered at compile time by TestMetadataGenerator public class CalculatorTests { [Test] public async Task Addition_Should_Return_Sum() { // Arrange var calculator = new Calculator(); // Act var result = calculator.Add(2, 3); // Assert await Assert.That(result).IsEqualTo(5); } [Test] public async Task Division_By_Zero_Should_Throw() { var calculator = new Calculator(); await Assert.That(() => calculator.Divide(10, 0)) .Throws(); } } ``` ### Data-Driven Testing with Arguments ```csharp // Parameterized test with inline arguments [Test] [Arguments(1, 1, 2)] [Arguments(2, 3, 5)] [Arguments(10, -5, 5)] [Arguments(0, 0, 0)] public async Task Add_Should_Return_Correct_Sum(int a, int b, int expected) { var calculator = new Calculator(); var result = calculator.Add(a, b); await Assert.That(result).IsEqualTo(expected); } // Property-based data injection public class EnvironmentTests { [Arguments("Development")] [Arguments("Staging")] [Arguments("Production")] public string Environment { get; set; } = ""; [Test] public async Task Config_Should_Match_Environment() { // Property is injected before test execution var config = new ConfigService(Environment); await Assert.That(config.GetConnectionString()) .Contains(Environment); } } ``` ### Method Data Source ```csharp [Test] [MethodDataSource(nameof(GetTestCases))] public async Task Validate_User_Data(string name, int age, bool expectedValid) { var user = new User { Name = name, Age = age }; var validator = new UserValidator(); var isValid = validator.Validate(user); await Assert.That(isValid).IsEqualTo(expectedValid); } // Data source can return IEnumerable of tuples public static IEnumerable<(string, int, bool)> GetTestCases() { yield return ("John Doe", 25, true); yield return ("", 30, false); // Empty name invalid yield return ("Jane", -5, false); // Negative age invalid yield return ("Bob", 150, false); // Age too high } // Method data source with async operation [Test] [MethodDataSource(nameof(GetAsyncTestData))] public async Task Process_User_Async(User user) { var service = new UserService(); var result = await service.ProcessAsync(user); await Assert.That(result).IsNotNull(); } public static async IAsyncEnumerable GetAsyncTestData() { await Task.Delay(10); // Simulate async data loading yield return new User { Name = "Alice", Age = 30 }; yield return new User { Name = "Bob", Age = 25 }; } ``` ### Class Data Source with Constructor Injection ```csharp [ClassDataSource] public class UserServiceTests { private readonly User _testUser; private readonly UserService _service; // Constructor receives injected data from ClassDataSource public UserServiceTests(User testUser) { _testUser = testUser; _service = new UserService(); } [Test] public async Task Validate_Should_Pass_For_Valid_Users() { var isValid = _service.Validate(_testUser); await Assert.That(isValid).IsTrue(); } [Test] public async Task GetAge_Should_Return_Positive_Number() { var age = _service.CalculateAge(_testUser); await Assert.That(age).IsGreaterThanOrEqualTo(0); } } public class UserTestData : IEnumerable { public IEnumerator GetEnumerator() { yield return new User { Name = "Test User 1", BirthDate = new DateTime(1990, 1, 1) }; yield return new User { Name = "Test User 2", BirthDate = new DateTime(2000, 6, 15) }; yield return new User { Name = "Test User 3", BirthDate = new DateTime(1985, 12, 31) }; } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); } ``` ### Matrix Data Sources for Combinatorial Testing ```csharp // Matrix testing - creates test for every combination [Test] [MatrixDataSource] public async Task Database_Operations_Should_Work_Across_Configurations( [Matrix("Create", "Read", "Update", "Delete")] string operation, [Matrix("User", "Product", "Order")] string entity, [Matrix("SqlServer", "PostgreSQL", "MySQL")] string database) { var dbContext = DatabaseFactory.Create(database); var result = await ExecuteOperation(dbContext, operation, entity); await Assert.That(result.Success).IsTrue(); } // Generates 4 x 3 x 3 = 36 test cases automatically ``` ### Fluent Assertions API ```csharp // Value assertions [Test] public async Task Value_Assertions() { await Assert.That(42).IsEqualTo(42); await Assert.That(100).IsGreaterThan(50); await Assert.That(10).IsLessThanOrEqualTo(10); await Assert.That("hello").IsEqualTo("hello"); await Assert.That((object?)null).IsNull(); await Assert.That("test").IsNotNull(); } // String assertions [Test] public async Task String_Assertions() { await Assert.That("Hello World").StartsWith("Hello"); await Assert.That("test@example.com").EndsWith(".com"); await Assert.That("Hello World").Contains("World"); await Assert.That(" spaces ").IsNotNullOrWhiteSpace(); } // Collection assertions [Test] public async Task Collection_Assertions() { var numbers = new[] { 1, 2, 3, 4, 5 }; await Assert.That(numbers).Contains(3); await Assert.That(numbers).HasCount().EqualTo(5); await Assert.That(numbers).IsInOrder(); await Assert.That(numbers).All(x => x > 0); } // Dictionary assertions [Test] public async Task Dictionary_Assertions() { var dict = new Dictionary { ["apple"] = 1, ["banana"] = 2, ["cherry"] = 3 }; await Assert.That(dict).ContainsKey("apple"); await Assert.That(dict).ContainsValue(2); await Assert.That(dict).HasCount().EqualTo(3); } // Chaining assertions with And [Test] public async Task Chained_Assertions() { var random = new Random(); var value = random.Next(1, 100); await Assert.That(value) .IsGreaterThan(0) .And.IsLessThanOrEqualTo(100); } ``` ### Exception Assertions ```csharp [Test] public async Task Exception_Assertions() { // Async exception testing await Assert.That(async () => await ThrowingMethodAsync()) .Throws(); // With message validation await Assert.That(async () => await ThrowingMethodAsync()) .Throws() .WithMessage("Something went wrong"); // Synchronous exception testing var exception = Assert.Throws( () => new UserService(null!)); await Assert.That(exception.ParamName).IsEqualTo("config"); } [Test] public async Task ThrowsExactly_Should_Not_Match_Subclasses() { // ThrowsExactly requires exact type match await Assert.That(() => throw new ArgumentNullException()) .ThrowsExactly(); // This would fail because ArgumentNullException is subclass of ArgumentException // await Assert.That(() => throw new ArgumentNullException()) // .ThrowsExactly(); // FAILS } ``` ### Test Lifecycle Hooks ```csharp public class DatabaseTests { private static DatabaseConnection? _connection; // Runs once before any tests in the assembly [Before(HookType.Assembly)] public static async Task SetupDatabase() { _connection = await DatabaseConnection.OpenAsync("test-db"); } // Runs once after all tests in the assembly [After(HookType.Assembly)] public static async Task TeardownDatabase() { if (_connection != null) await _connection.CloseAsync(); } // Runs once before all tests in this class [Before(HookType.Class)] public static async Task SetupTestClass() { await _connection!.ExecuteAsync("TRUNCATE TABLE Users"); } // Runs before each test method [Before(HookType.Test)] public async Task SetupTest() { await _connection!.ExecuteAsync("BEGIN TRANSACTION"); } // Runs after each test method [After(HookType.Test)] public async Task CleanupTest() { await _connection!.ExecuteAsync("ROLLBACK"); } [Test] public async Task Insert_User_Should_Succeed() { var user = new User { Name = "Test", Email = "test@example.com" }; await _connection!.InsertAsync(user); var count = await _connection.QueryScalarAsync("SELECT COUNT(*) FROM Users"); await Assert.That(count).IsEqualTo(1); } } ``` ### Test Dependencies ```csharp public class IntegrationTests { [Test] public async Task Step1_Create_User() { var user = await UserService.CreateAsync("john@example.com"); await Assert.That(user.Id).IsGreaterThan(0); } // This test waits for Step1_Create_User to complete successfully [Test] [DependsOn(nameof(Step1_Create_User))] public async Task Step2_Login_With_Created_User() { var loginResult = await AuthService.LoginAsync("john@example.com", "password"); await Assert.That(loginResult.Success).IsTrue(); } // Depends on Step2 completing [Test] [DependsOn(nameof(Step2_Login_With_Created_User))] public async Task Step3_Access_Protected_Resource() { var resource = await ApiClient.GetProtectedResourceAsync(); await Assert.That(resource).IsNotNull(); } // Dependency on test in another class [Test] [DependsOn(nameof(DatabaseSetupTests.InitializeSchema))] public async Task Query_Requires_Schema() { var users = await UserRepository.GetAllAsync(); await Assert.That(users).IsNotNull(); } // Run even if dependency fails [Test] [DependsOn(nameof(Step1_Create_User), ProceedOnFailure = true)] public async Task Cleanup_Runs_Regardless() { await UserService.DeleteAllTestUsersAsync(); } } ``` ### Retry Logic ```csharp // Retry test up to 3 times on failure [Test] [Retry(3)] public async Task Flaky_Network_Operation() { var client = new HttpClient(); var response = await client.GetAsync("https://api.example.com/status"); await Assert.That(response.IsSuccessStatusCode).IsTrue(); } // Custom retry with conditional logic public class RetryOnHttpErrorAttribute : RetryAttribute { public RetryOnHttpErrorAttribute(int times) : base(times) { } public override Task ShouldRetry( TestContext context, Exception exception, int currentRetryCount) { // Only retry on specific HTTP errors return Task.FromResult(exception is HttpRequestException ex && ex.StatusCode == HttpStatusCode.ServiceUnavailable); } } [Test] [RetryOnHttpError(5)] public async Task Api_Call_Retries_On_503() { var result = await ApiClient.GetDataAsync(); await Assert.That(result).IsNotNull(); } ``` ### Repeat Tests ```csharp // Run test 100 times to check for flakiness [Test] [Repeat(100)] public async Task Performance_Test_Should_Be_Consistent() { var stopwatch = Stopwatch.StartNew(); await HeavyOperation(); stopwatch.Stop(); await Assert.That(stopwatch.ElapsedMilliseconds) .IsLessThan(1000); // Must complete in under 1 second } ``` ### Parallel Execution Control ```csharp // Custom parallel limit public class DatabaseParallelLimit : IParallelLimit { public int Limit => 5; // Maximum 5 concurrent database tests } [Test] [ParallelLimit] public async Task Database_Test_1() { // Only 5 database tests run concurrently await DatabaseOperation(); } // Tests in same parallel group don't run simultaneously [Test] [ParallelGroup("FileAccess")] public async Task Write_To_Shared_File_1() { await File.WriteAllTextAsync("shared.txt", "Test 1"); } [Test] [ParallelGroup("FileAccess")] public async Task Write_To_Shared_File_2() { // Won't run until Write_To_Shared_File_1 completes await File.WriteAllTextAsync("shared.txt", "Test 2"); } // Disable parallelism entirely for specific tests [Test] [NotInParallel] public async Task Sequential_Test() { // Runs alone, not parallel with any other test await CriticalOperation(); } ``` ### Custom Skip Conditions ```csharp public class WindowsOnlyAttribute : SkipAttribute { public WindowsOnlyAttribute() : base("Test only runs on Windows") { } public override Task ShouldSkip(TestContext testContext) => Task.FromResult(!OperatingSystem.IsWindows()); } public class RequiresDatabaseAttribute : SkipAttribute { public RequiresDatabaseAttribute() : base("Database not available") { } public override async Task ShouldSkip(TestContext testContext) { try { using var connection = await DatabaseConnection.TryConnectAsync(); return connection == null; } catch { return true; // Skip if connection fails } } } [Test] [WindowsOnly] public async Task Windows_Specific_Feature() { // Only runs on Windows platform await WindowsApi.CallAsync(); } [Test] [RequiresDatabase] public async Task Database_Integration_Test() { // Skipped if database not available await DatabaseRepository.QueryAsync(); } ``` ### Multiple Assertions ```csharp [Test] public async Task Multiple_Assertions_Should_All_Execute() { var user = new User { Name = "John Doe", Email = "john@example.com", Age = 30 }; // All assertions run even if earlier ones fail // All failures are reported together using (Assert.Multiple()) { await Assert.That(user.Name).IsEqualTo("John Doe"); await Assert.That(user.Email).Contains("@"); await Assert.That(user.Age).IsGreaterThanOrEqualTo(18); await Assert.That(user.Age).IsLessThan(150); } } ``` ### Test Context and Metadata ```csharp [Test] public async Task Access_Test_Context(TestContext context) { // TestContext is automatically injected context.Output.WriteLine("Starting test..."); await Assert.That(context.TestDetails.TestName) .IsEqualTo("Access_Test_Context"); // Write output visible in test results context.Output.WriteLine($"Test ID: {context.TestDetails.TestId}"); // Access test metadata var categories = context.TestDetails.Categories; await Assert.That(categories).IsNotNull(); } [Test] [Category("Integration")] [Category("Database")] [Property("Priority", "High")] public async Task Test_With_Metadata() { // Categories and properties can be used for test filtering await DatabaseOperation(); } ``` ### Task and ValueTask Assertions ```csharp [Test] public async Task Task_Assertions() { var task = LongRunningOperationAsync(); // Assert on task completion await Assert.That(task).IsCompleted(); // Assert on task result await Assert.That(task).IsEqualTo(expectedValue); // Assert task throws exception await Assert.That(Task.FromException(new InvalidOperationException())) .Throws(); } [Test] public async Task ValueTask_Assertions() { var valueTask = GetValueAsync(); await Assert.That(valueTask.AsTask()).IsEqualTo(42); } ``` ### Null Safety Assertions ```csharp [Test] public void Null_Check_Affects_Compiler_Analysis() { string? nullableString = GetPotentiallyNullString(); // After this, compiler treats nullableString as non-null Assert.NotNull(nullableString); // No null warning here int length = nullableString.Length; // Alternative: fluent API (doesn't affect null analysis) // await Assert.That(nullableString).IsNotNull(); } ``` ### Custom Data Source Generators ```csharp // Create reusable data source generator public class RandomNumberGeneratorAttribute : DataSourceGeneratorAttribute { private readonly int _count; private readonly int _min; private readonly int _max; public RandomNumberGeneratorAttribute(int count, int min, int max) { _count = count; _min = min; _max = max; } protected override IEnumerable> GenerateDataSources( DataGeneratorMetadata dataGeneratorMetadata) { var random = new Random(); for (int i = 0; i < _count; i++) { yield return () => random.Next(_min, _max); } } } [Test] [RandomNumberGenerator(10, 1, 100)] public async Task Test_With_Random_Numbers(int randomValue) { // Runs 10 times with different random numbers between 1 and 100 await Assert.That(randomValue).IsGreaterThan(0).And.IsLessThan(100); } ``` ### Timeout Control ```csharp [Test] [Timeout(5000)] // 5 seconds public async Task Operation_Should_Complete_Quickly() { await FastOperation(); // Test fails if not completed within 5 seconds } [Test] [Timeout(60000)] // 1 minute public async Task Long_Running_Operation() { await ExpensiveOperation(); } ``` ### Display Names and Categories ```csharp [Test] [DisplayName("User registration should create new account")] [Category("Authentication")] [Category("UserManagement")] public async Task Register_User() { var result = await AuthService.RegisterAsync("user@example.com", "password"); await Assert.That(result.Success).IsTrue(); } // Custom display name formatter public class CustomDisplayNameFormatter : DisplayNameFormatterAttribute { public override string Format(TestContext context) { return $"[{context.TestDetails.Categories[0]}] {context.TestDetails.TestName}"; } } [Test] [CustomDisplayNameFormatter] [Category("API")] public async Task Get_Users() { } // Displays as: "[API] Get_Users" ``` ## Integration Patterns and Use Cases TUnit excels in both unit testing and complex integration scenarios. For unit testing, the framework provides fast test execution with minimal overhead through compile-time discovery. Tests run in parallel by default, dramatically reducing suite execution time for large codebases. The fluent assertion API supports readable test code with comprehensive validation options for values, collections, strings, exceptions, and tasks. TUnit's source generator architecture ensures zero reflection overhead during test discovery, making it ideal for performance-critical test suites that need to run frequently in CI/CD pipelines. For integration testing, TUnit's test dependency system enables sophisticated test orchestration where tests can depend on successful completion of prerequisite tests. The framework's lifecycle hooks (Before/After at Test, Class, Assembly, and TestSession levels) provide precise control over resource setup and teardown. Data-driven testing capabilities support multiple data source types including inline arguments, method-based sources, class-based sources, and matrix combinations for exhaustive scenario coverage. The parallel execution control features (ParallelLimit, ParallelGroup, NotInParallel) allow fine-grained management of concurrent test execution when dealing with shared resources like databases, file systems, or external APIs. Combined with retry logic, timeout controls, and conditional skip attributes, TUnit provides a complete testing solution for modern .NET applications ranging from simple unit tests to complex distributed system integration scenarios.