From 988fdc8bebc4b7bb7b2f83ba2704133e6671ecc4 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:11:26 +0000 Subject: [PATCH 1/9] docs: fix code bugs, API inconsistencies, and phantom properties across all docs - Add missing `await` before assertions in nested-data-sources.md - Remove shell heredoc artifact from method-data-source.md - Fix syntax error (missing paren) in assertions/getting-started.md - Fix missing bracket in comparison/attributes.md - Normalize hook syntax to short form [After(Test)] across 8 files - Fix TestDetails.TestName to Metadata.TestName in user-facing examples - Fix IAsyncInitializable to IAsyncInitializer in aot-compatibility.md - Fix OutputWriter.WriteLine to Output.WriteLine in migration guides - Fix ITestExecutor signature to ValueTask ExecuteTest in 2 files - Remove phantom TUnitCacheTestResults MSBuild properties - Fix ConcurrentDictionary.GetOrAddAsync to correct pattern - Fix chaos engineering example parameter mismatch in test-variants.md - Fix GitLab CI TRX/JUnit format error in ci-cd-reporting.md --- docs/docs/advanced/exception-handling.md | 16 +++---- .../advanced/performance-best-practices.md | 42 +++++++------------ docs/docs/advanced/test-variants.md | 17 +++++--- docs/docs/assertions/getting-started.md | 2 +- docs/docs/comparison/attributes.md | 2 +- .../customization-extensibility/logging.md | 10 ++--- docs/docs/examples/aspnet.md | 10 ++--- .../examples/complex-test-infrastructure.md | 2 +- .../examples/instrumenting-global-test-ids.md | 2 +- docs/docs/execution/ci-cd-reporting.md | 8 +++- docs/docs/migration/mstest.md | 38 ++++++++--------- docs/docs/migration/nunit.md | 20 ++++----- .../testcontext-interface-organization.md | 4 +- docs/docs/migration/xunit.md | 10 ++--- docs/docs/test-authoring/aot-compatibility.md | 4 +- .../docs/test-authoring/method-data-source.md | 1 - .../test-authoring/nested-data-sources.md | 2 +- docs/docs/test-lifecycle/artifacts.md | 20 ++++----- docs/docs/troubleshooting.md | 28 ++++++------- 19 files changed, 119 insertions(+), 119 deletions(-) diff --git a/docs/docs/advanced/exception-handling.md b/docs/docs/advanced/exception-handling.md index 4868dffd8f..e80297dd1e 100644 --- a/docs/docs/advanced/exception-handling.md +++ b/docs/docs/advanced/exception-handling.md @@ -37,10 +37,10 @@ Hook exceptions are thrown when setup or cleanup operations fail. Each hook type #### BeforeTestException / AfterTestException -Thrown when a `[Before(HookType.Test)]` or `[After(HookType.Test)]` hook fails. +Thrown when a `[Before(Test)]` or `[After(Test)]` hook fails. ```csharp -[Before(HookType.Test)] +[Before(Test)] public async Task TestSetup() { // If this throws, it will be wrapped in BeforeTestException @@ -68,7 +68,7 @@ public async Task MyTest() Thrown when class-level hooks fail. These affect all tests in the class. ```csharp -[Before(HookType.Class)] +[Before(Class)] public static async Task ClassSetup() { // If this fails, all tests in the class will be marked as failed @@ -206,11 +206,11 @@ When implementing custom test executors or hook executors, proper exception hand ```csharp public class SafeTestExecutor : ITestExecutor { - public async Task ExecuteAsync(TestContext context, Func testBody) + public async ValueTask ExecuteTest(TestContext context, Func action) { try { - await testBody(); + await action(); } catch (TUnitException) { @@ -226,7 +226,7 @@ public class SafeTestExecutor : ITestExecutor { // Wrap other exceptions with context throw new TestExecutionException( - $"Test '{context.TestName}' failed with unexpected exception", + $"Test '{context.Metadata.TestName}' failed with unexpected exception", ex); } } @@ -408,7 +408,7 @@ public async Task ExceptionResultTest() } } -[After(HookType.Test)] +[After(Test)] public async Task LogTestExceptions() { var result = TestContext.Current?.Result; @@ -480,7 +480,7 @@ public async Task ResourceTest() ### Scenario: Conditional Test Execution ```csharp -[Before(HookType.Test)] +[Before(Test)] public async Task CheckEnvironment() { if (!IsCorrectEnvironment()) diff --git a/docs/docs/advanced/performance-best-practices.md b/docs/docs/advanced/performance-best-practices.md index b2b4502404..1081962465 100644 --- a/docs/docs/advanced/performance-best-practices.md +++ b/docs/docs/advanced/performance-best-practices.md @@ -167,7 +167,7 @@ public class DatabaseIntegrationTests // ❌ Bad: Expensive setup per test public class ExpensiveTests { - [Before(HookType.Test)] + [Before(Test)] public async Task SetupEachTest() { await StartDatabaseContainer(); @@ -180,14 +180,14 @@ public class EfficientTests { private static DatabaseContainer? _container; - [Before(HookType.Class)] + [Before(Class)] public static async Task SetupOnce() { _container = await StartDatabaseContainer(); await MigrateDatabase(); } - [After(HookType.Class)] + [After(Class)] public static async Task CleanupOnce() { if (_container != null) @@ -292,7 +292,7 @@ public class LeakyTests { private static readonly List _allResults = new(); - [After(HookType.Test)] + [After(Test)] public void StoreResult() { _allResults.Add(GetCurrentResult()); // Memory leak! @@ -305,7 +305,7 @@ public class EfficientTests private static readonly Queue _recentResults = new(); private const int MaxResults = 100; - [After(HookType.Test)] + [After(Test)] public void StoreResult() { _recentResults.Enqueue(GetCurrentResult()); @@ -395,14 +395,14 @@ public async Task AsyncIOTest() ```csharp public class FileTestsWithCache { - private static readonly ConcurrentDictionary _fileCache = new(); - - private async Task GetFileContentAsync(string path) + private static readonly ConcurrentDictionary>> _fileCache = new(); + + private Task GetFileContentAsync(string path) { - return await _fileCache.GetOrAddAsync(path, - async p => await File.ReadAllTextAsync(p)); + return _fileCache.GetOrAdd(path, + p => new Lazy>(() => File.ReadAllTextAsync(p))).Value; } - + [Test] [Arguments("config1.json")] [Arguments("config2.json")] @@ -496,16 +496,6 @@ dotnet test --no-build -- --treenode-filter "/*/*/*/*[Category=E2E]" > dotnet test --no-build --treenode-filter "/**[Category=Unit]" > ``` -### Use Test Result Caching - -```xml - - - true - $(Build.StagingDirectory)/testcache - -``` - ### Fail Fast in CI ```bash @@ -522,13 +512,13 @@ public class PerformanceAwareExecutor : ITestExecutor { private readonly ILogger _logger; - public async Task ExecuteAsync(TestContext context, Func testBody) + public async ValueTask ExecuteTest(TestContext context, Func action) { var stopwatch = Stopwatch.StartNew(); - + try { - await testBody(); + await action(); } finally { @@ -549,7 +539,7 @@ public class PerformanceAwareExecutor : ITestExecutor ### Track Test Metrics ```csharp -[After(HookType.Test)] +[After(Test)] public static void RecordTestMetrics() { var context = TestContext.Current; @@ -561,7 +551,7 @@ public static void RecordTestMetrics() new Dictionary { ["TestName"] = context.Metadata.TestName, - ["TestClass"] = context.Metadata.TestDetails.TestClass, + ["TestClass"] = context.Metadata.TestDetails.ClassType.Name, ["Result"] = context.Execution.Result.State.ToString() }); } diff --git a/docs/docs/advanced/test-variants.md b/docs/docs/advanced/test-variants.md index 9bb698c504..0a34081955 100644 --- a/docs/docs/advanced/test-variants.md +++ b/docs/docs/advanced/test-variants.md @@ -312,12 +312,19 @@ Inject faults and verify system resilience: ```csharp [Test] -public async Task Resilience_DatabaseFailover() +[Arguments("none")] +public async Task Resilience_DatabaseFailover(string faultType) { var context = TestContext.Current!; var system = new DistributedSystem(); - // Normal operation test + // Inject fault if specified + if (faultType != "none") + { + system.InjectFault(faultType); + } + + // Test system behavior var result = await system.ProcessRequestAsync(); await Assert.That(result.Success).IsTrue(); @@ -330,13 +337,13 @@ public async Task Resilience_DatabaseFailover() ("cascading-failure", "Cascading Failure") }; - foreach (var (faultType, displayName) in chaosScenarios) + foreach (var (scenario, displayName) in chaosScenarios) { await context.CreateTestVariant( - arguments: new object[] { faultType }, + arguments: new object[] { scenario }, properties: new Dictionary { - { "ChaosType", faultType }, + { "ChaosType", scenario }, { "InjectionPoint", "AfterSuccess" } }, relationship: TestRelationship.Derived, diff --git a/docs/docs/assertions/getting-started.md b/docs/docs/assertions/getting-started.md index 7ccbd2612d..9900306af2 100644 --- a/docs/docs/assertions/getting-started.md +++ b/docs/docs/assertions/getting-started.md @@ -142,7 +142,7 @@ This works with nested properties too: ```csharp await Assert.That(order) - .Member(o => o.Customer.Address.City, city => city.IsEqualTo("Seattle"); + .Member(o => o.Customer.Address.City, city => city.IsEqualTo("Seattle")); ``` ## Working with Collections diff --git a/docs/docs/comparison/attributes.md b/docs/docs/comparison/attributes.md index 3553f36bcf..c15a8c6c27 100644 --- a/docs/docs/comparison/attributes.md +++ b/docs/docs/comparison/attributes.md @@ -65,4 +65,4 @@ Here are TUnit's equivalent attributes to other test frameworks. |--------------------|-------|------------------------|--------| | [Culture("en-US")] | - | [SetCulture("en-US")] | - | | - | - | [Culture("en-US")] | - | -| - | - | [SetUICulture("en-US") | - | +| - | - | [SetUICulture("en-US")] | - | diff --git a/docs/docs/customization-extensibility/logging.md b/docs/docs/customization-extensibility/logging.md index c58d9a468f..ab6cc379f0 100644 --- a/docs/docs/customization-extensibility/logging.md +++ b/docs/docs/customization-extensibility/logging.md @@ -76,7 +76,7 @@ public class FileLogSink : ILogSink, IAsyncDisposable { // Get test name from context if available var testName = context is TestContext tc - ? tc.TestDetails.TestName + ? tc.Metadata.TestName : "Unknown"; _writer.WriteLine($"[{DateTime.Now:HH:mm:ss}] [{level}] [{testName}] {message}"); @@ -133,8 +133,8 @@ public void Log(LogLevel level, string message, Exception? exception, Context? c { case TestContext tc: // During test execution - var testName = tc.TestDetails.TestName; - var className = tc.TestDetails.ClassType.Name; + var testName = tc.Metadata.TestName; + var className = tc.Metadata.TestDetails.ClassType.Name; break; case ClassHookContext chc: @@ -185,7 +185,7 @@ public class SeqLogSink : ILogSink, IDisposable _ => Serilog.Events.LogEventLevel.Information }; - var testName = context is TestContext tc ? tc.TestDetails.TestName : "Unknown"; + var testName = context is TestContext tc ? tc.Metadata.TestName : "Unknown"; _logger .ForContext("TestName", testName) @@ -276,7 +276,7 @@ public class TestHeaderLogger : DefaultLogger if (!_hasOutputHeader && Context is TestContext testContext) { _hasOutputHeader = true; - var testId = $"{testContext.TestDetails.ClassType.Name}.{testContext.TestDetails.TestName}"; + var testId = $"{testContext.Metadata.TestDetails.ClassType.Name}.{testContext.Metadata.TestName}"; return $"--- {testId} ---\n{baseMessage}"; } diff --git a/docs/docs/examples/aspnet.md b/docs/docs/examples/aspnet.md index bbe642641a..01ac899279 100644 --- a/docs/docs/examples/aspnet.md +++ b/docs/docs/examples/aspnet.md @@ -322,7 +322,7 @@ public abstract class TodoTestBase : TestsBase }); } - [After(HookType.Test)] + [After(Test)] public async Task CleanupTable() { await DropTableAsync(TableName); @@ -412,7 +412,7 @@ public abstract class EfCoreTodoTestBase : WebApplicationTest await DropTableAsync(); private async Task CreateTableAsync() { /* ... */ } @@ -825,7 +825,7 @@ protected override void ConfigureTestConfiguration(IConfigurationBuilder config) **Solution:** This is by design. `ConfigureTestOptions` runs before `SetupAsync` because test options affect how the infrastructure is set up. If you need async setup before options, consider: -1. Moving the logic to a `[Before(HookType.Test)]` method that runs even earlier +1. Moving the logic to a `[Before(Test)]` method that runs even earlier 2. Using lazy initialization in `SetupAsync` ### Why are my parallel tests interfering with each other? diff --git a/docs/docs/examples/complex-test-infrastructure.md b/docs/docs/examples/complex-test-infrastructure.md index 151b5a7a35..4ef83bf68c 100644 --- a/docs/docs/examples/complex-test-infrastructure.md +++ b/docs/docs/examples/complex-test-infrastructure.md @@ -271,7 +271,7 @@ public abstract class EfCoreTodoTestBase }); } - [After(HookType.Test)] + [After(Test)] public async Task CleanupSchema() { await using var conn = new NpgsqlConnection( diff --git a/docs/docs/examples/instrumenting-global-test-ids.md b/docs/docs/examples/instrumenting-global-test-ids.md index 39517c706e..f3e9b514b7 100644 --- a/docs/docs/examples/instrumenting-global-test-ids.md +++ b/docs/docs/examples/instrumenting-global-test-ids.md @@ -53,7 +53,7 @@ class MyTestClassThatNeedsUniqueTestIds { private IDatabase isolatedRedisDb = null!; - [Before(HookType.Test)] + [Before(Test)] public void BeforeEach() { // Call the extension method to retrieve the unique test id: diff --git a/docs/docs/execution/ci-cd-reporting.md b/docs/docs/execution/ci-cd-reporting.md index 7a9bad90d6..e94dd0e33d 100644 --- a/docs/docs/execution/ci-cd-reporting.md +++ b/docs/docs/execution/ci-cd-reporting.md @@ -189,18 +189,22 @@ dotnet test --logger "console;verbosity=detailed" ### GitLab CI -GitLab can parse test results in various formats: +GitLab's test report feature expects JUnit XML format. TRX files are **not** JUnit XML, so you need a conversion step: ```yaml test: script: - dotnet test -- --report-trx + - dotnet tool install -g trx2junit + - trx2junit TestResults/*.trx artifacts: reports: junit: - - TestResults/*.trx + - TestResults/*.xml ``` +The `trx2junit` tool converts TRX output to JUnit XML that GitLab can parse. + ## Environment Detection TUnit automatically detects common CI environments through environment variables: diff --git a/docs/docs/migration/mstest.md b/docs/docs/migration/mstest.md index 84c01b828c..cb829df2a4 100644 --- a/docs/docs/migration/mstest.md +++ b/docs/docs/migration/mstest.md @@ -207,7 +207,7 @@ dotnet run -- --list-tests **TestContext errors:** - Remove the `public TestContext TestContext { get; set; }` property - Add `TestContext context` parameter to test methods that need it -- Access output via `context.OutputWriter.WriteLine(...)` instead of `TestContext.WriteLine(...)` +- Access output via `context.Output.WriteLine(...)` instead of `TestContext.WriteLine(...)` **ClassInitialize/AssemblyInitialize errors:** - Remove the `TestContext context` parameter from these methods @@ -239,17 +239,17 @@ dotnet run -- --list-tests ### Setup and Teardown -`[TestInitialize]` becomes `[Before(HookType.Test)]` +`[TestInitialize]` becomes `[Before(Test)]` -`[TestCleanup]` becomes `[After(HookType.Test)]` +`[TestCleanup]` becomes `[After(Test)]` -`[ClassInitialize]` becomes `[Before(HookType.Class)]` and remove the TestContext parameter +`[ClassInitialize]` becomes `[Before(Class)]` and remove the TestContext parameter -`[ClassCleanup]` becomes `[After(HookType.Class)]` +`[ClassCleanup]` becomes `[After(Class)]` -`[AssemblyInitialize]` becomes `[Before(HookType.Assembly)]` and remove the TestContext parameter +`[AssemblyInitialize]` becomes `[Before(Assembly)]` and remove the TestContext parameter -`[AssemblyCleanup]` becomes `[After(HookType.Assembly)]` +`[AssemblyCleanup]` becomes `[After(Assembly)]` ### Assertions @@ -422,10 +422,10 @@ public class MyTests [Test] public async Task MyTest(TestContext context) { - context.OutputWriter.WriteLine("Test output"); + context.Output.WriteLine("Test output"); } - [Before(HookType.Class)] + [Before(Class)] public static async Task ClassInit() { // Setup code - no TestContext parameter needed @@ -610,7 +610,7 @@ public class OrderServiceTests { // Runs before each test _orderService = new OrderService(_sharedDatabase); - context.OutputWriter.WriteLine("Starting test"); + context.Output.WriteLine("Starting test"); } [Test] @@ -626,7 +626,7 @@ public class OrderServiceTests await Assert.That(order.ProductName).IsEqualTo(productName); await Assert.That(order.Price).IsEqualTo((decimal)price); - context.OutputWriter.WriteLine($"Order created: {order.Id}"); + context.Output.WriteLine($"Order created: {order.Id}"); } [Test] @@ -656,7 +656,7 @@ public class OrderServiceTests { // Runs after each test _orderService?.Dispose(); - context.OutputWriter.WriteLine("Test completed"); + context.Output.WriteLine("Test completed"); } [After(Class)] @@ -1055,18 +1055,18 @@ public class ContextTests public async Task UsingTestContext_AllProperties(TestContext context) { // Writing output - context.OutputWriter.WriteLine($"Test: {context.Metadata.TestName}"); - context.OutputWriter.WriteLine($"Test ID: {context.Metadata.TestDetails.TestId}"); + context.Output.WriteLine($"Test: {context.Metadata.TestName}"); + context.Output.WriteLine($"Test ID: {context.Metadata.TestDetails.TestId}"); // Accessing test details - context.OutputWriter.WriteLine($"Class: {context.Metadata.TestDetails.ClassType.Name}"); - context.OutputWriter.WriteLine($"Method: {context.Metadata.TestDetails.MethodInfo.Name}"); + context.Output.WriteLine($"Class: {context.Metadata.TestDetails.ClassType.Name}"); + context.Output.WriteLine($"Method: {context.Metadata.TestDetails.MethodInfo.Name}"); // Accessing attributes and properties var properties = context.Metadata.TestDetails.Attributes.OfType(); foreach (var prop in properties) { - context.OutputWriter.WriteLine($"{prop.Key}: {prop.Value}"); + context.Output.WriteLine($"{prop.Key}: {prop.Value}"); } await Assert.That(true).IsTrue(); @@ -1085,14 +1085,14 @@ public class ContextTests .OfType() .FirstOrDefault(p => p.Key == "Environment"); - context.OutputWriter.WriteLine($"Running on {browserProp?.Value} in {envProp?.Value}"); + context.Output.WriteLine($"Running on {browserProp?.Value} in {envProp?.Value}"); } } ``` **Key Changes:** - TestContext is injected as parameter, not a property -- Access output via `context.OutputWriter.WriteLine()` +- Access output via `context.Output.WriteLine()` - Test metadata available via `context.Metadata.TestDetails` - Properties accessed through attributes rather than dictionary - More type-safe property access diff --git a/docs/docs/migration/nunit.md b/docs/docs/migration/nunit.md index 563cbf46b0..e5d77f08ef 100644 --- a/docs/docs/migration/nunit.md +++ b/docs/docs/migration/nunit.md @@ -24,7 +24,7 @@ Migrating from NUnit to TUnit can improve test execution speed. Check the [bench | `Assert.AreEqual(expected, actual)` | `await Assert.That(actual).IsEqualTo(expected)` | | `Assert.That(actual, Is.EqualTo(expected))` | `await Assert.That(actual).IsEqualTo(expected)` | | `Assert.Throws(() => ...)` | `await Assert.ThrowsAsync(() => ...)` | -| `TestContext.WriteLine(...)` | `TestContext` parameter with `context.OutputWriter.WriteLine(...)` | +| `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)` | @@ -214,13 +214,13 @@ dotnet run -- --list-tests ### Setup and Teardown -`[SetUp]` becomes `[Before(HookType.Test)]` +`[SetUp]` becomes `[Before(Test)]` -`[TearDown]` becomes `[After(HookType.Test)]` +`[TearDown]` becomes `[After(Test)]` -`[OneTimeSetUp]` becomes `[Before(HookType.Class)]` +`[OneTimeSetUp]` becomes `[Before(Class)]` -`[OneTimeTearDown]` becomes `[After(HookType.Class)]` +`[OneTimeTearDown]` becomes `[After(Class)]` ### Assertions @@ -358,8 +358,8 @@ TestContext.Out.WriteLine("More output"); // TUnit (inject TestContext) public async Task MyTest(TestContext context) { - context.OutputWriter.WriteLine("Test output"); - context.OutputWriter.WriteLine("More output"); + context.Output.WriteLine("Test output"); + context.Output.WriteLine("More output"); } ``` @@ -692,9 +692,9 @@ public void Test_WithContextProperties() [Test] public async Task Test_WithContextProperties(TestContext context) { - context.OutputWriter.WriteLine($"Test Name: {context.Metadata.TestName}"); - context.OutputWriter.WriteLine($"Test ID: {context.Metadata.TestDetails.TestId}"); - context.OutputWriter.WriteLine($"Class Name: {context.Metadata.TestDetails.ClassType.Name}"); + 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 } diff --git a/docs/docs/migration/testcontext-interface-organization.md b/docs/docs/migration/testcontext-interface-organization.md index a408d112cc..d81f8bb29d 100644 --- a/docs/docs/migration/testcontext-interface-organization.md +++ b/docs/docs/migration/testcontext-interface-organization.md @@ -212,7 +212,7 @@ public class CustomTestBuilder **Before:** ```csharp -[Before(HookType.Test)] +[Before(Test)] public void Setup() { var externalCts = new CancellationTokenSource(); @@ -224,7 +224,7 @@ public void Setup() **After:** ```csharp -[Before(HookType.Test)] +[Before(Test)] public void Setup() { var externalCts = new CancellationTokenSource(); diff --git a/docs/docs/migration/xunit.md b/docs/docs/migration/xunit.md index 767ece1126..f059e84a89 100644 --- a/docs/docs/migration/xunit.md +++ b/docs/docs/migration/xunit.md @@ -795,11 +795,11 @@ public class LoggingTests [Test] public async Task Test_WithLogging(TestContext context) { - context.OutputWriter.WriteLine("Starting test"); + context.Output.WriteLine("Starting test"); var result = PerformOperation(); - context.OutputWriter.WriteLine($"Result: {result}"); + context.Output.WriteLine($"Result: {result}"); await Assert.That(result).IsGreaterThan(0); } } @@ -807,7 +807,7 @@ public class LoggingTests **Key Changes:** - `ITestOutputHelper` injected in constructor → `TestContext` injected as method parameter -- Access output via `context.OutputWriter.WriteLine()` +- Access output via `context.Output.WriteLine()` - TestContext provides additional test metadata #### Test Attachments @@ -1122,7 +1122,7 @@ public class UserServiceTests(DatabaseFixture dbFixture) [Arguments("jane@example.com", "Jane")] public async Task CreateUser_WithValidData_Succeeds(string email, string name, TestContext context) { - context.OutputWriter.WriteLine($"Creating user: {name}"); + context.Output.WriteLine($"Creating user: {name}"); var user = await _userService.CreateUserAsync(email, name); @@ -1130,7 +1130,7 @@ public class UserServiceTests(DatabaseFixture dbFixture) await Assert.That(user.Email).IsEqualTo(email); await Assert.That(user.Name).IsEqualTo(name); - context.OutputWriter.WriteLine($"User created with ID: {user.Id}"); + context.Output.WriteLine($"User created with ID: {user.Id}"); } [Test] diff --git a/docs/docs/test-authoring/aot-compatibility.md b/docs/docs/test-authoring/aot-compatibility.md index be30ef13e6..1fc204c1c5 100644 --- a/docs/docs/test-authoring/aot-compatibility.md +++ b/docs/docs/test-authoring/aot-compatibility.md @@ -214,7 +214,7 @@ public class LoggingService ### Async Property Initialization -Properties can implement `IAsyncInitializable` for complex setup: +Properties can implement `IAsyncInitializer` for complex setup: ```csharp using TUnit.Core; @@ -235,7 +235,7 @@ public class AsyncInitializationTests } } -public class AsyncContainer : IAsyncInitializable, IAsyncDisposable +public class AsyncContainer : IAsyncInitializer, IAsyncDisposable { public bool IsInitialized { get; private set; } public string ConnectionString { get; private set; } = ""; diff --git a/docs/docs/test-authoring/method-data-source.md b/docs/docs/test-authoring/method-data-source.md index e1db097ae8..92a6906e93 100644 --- a/docs/docs/test-authoring/method-data-source.md +++ b/docs/docs/test-authoring/method-data-source.md @@ -272,4 +272,3 @@ public class MyTests 2. **Alternative:** Use `IAsyncDiscoveryInitializer` if you need discovery-time initialization See [Property Injection - Discovery Phase Initialization](../test-lifecycle/property-injection.md#discovery-phase-initialization) for detailed guidance and examples. -EOF < /dev/null diff --git a/docs/docs/test-authoring/nested-data-sources.md b/docs/docs/test-authoring/nested-data-sources.md index cbce5a9832..802cbbae60 100644 --- a/docs/docs/test-authoring/nested-data-sources.md +++ b/docs/docs/test-authoring/nested-data-sources.md @@ -111,7 +111,7 @@ public class UserApiTests var redis = services.GetRequiredService(); var cached = await redis.GetDatabase().StringGetAsync("user:john@example.com"); - Assert.That(cached.HasValue).IsTrue(); + await Assert.That(cached.HasValue).IsTrue(); } } ``` diff --git a/docs/docs/test-lifecycle/artifacts.md b/docs/docs/test-lifecycle/artifacts.md index ddba738162..4a2cc6bfcc 100644 --- a/docs/docs/test-lifecycle/artifacts.md +++ b/docs/docs/test-lifecycle/artifacts.md @@ -58,7 +58,7 @@ A common pattern is to capture a screenshot when a test fails: ```csharp public class MyTests { - [After(HookType.Test)] + [After(Test)] public async Task TakeScreenshotOnFailure() { var testContext = TestContext.Current; @@ -72,7 +72,7 @@ public class MyTests { File = new FileInfo(screenshotPath), DisplayName = "Failure Screenshot", - Description = $"Screenshot captured when test '{testContext.TestDetails.TestName}' failed" + Description = $"Screenshot captured when test '{testContext.Metadata.TestName}' failed" }); } } @@ -131,7 +131,7 @@ Attach files to the entire test session using `TestSessionContext.Current.AddArt ### Basic Usage ```csharp -[Before(HookType.TestSession)] +[Before(TestSession)] public static void SetupTestSession() { // Start capturing session-wide logs @@ -153,7 +153,7 @@ public static void SetupTestSession() Attach configuration files to document the test environment: ```csharp -[Before(HookType.TestSession)] +[Before(TestSession)] public static void DocumentTestEnvironment() { // Attach environment configuration @@ -182,7 +182,7 @@ public static void DocumentTestEnvironment() Generate and attach performance reports for the entire test session: ```csharp -[After(HookType.TestSession)] +[After(TestSession)] public static void GeneratePerformanceReport() { // Generate performance report after all tests complete @@ -222,7 +222,7 @@ public class Artifact Consider cleaning up temporary artifact files after test execution to avoid accumulating files: ```csharp -[After(HookType.TestSession)] +[After(TestSession)] public static void CleanupArtifacts() { var artifactDir = "test-artifacts"; @@ -238,10 +238,10 @@ public static void CleanupArtifacts() Create a unique directory for each test's artifacts: ```csharp -[Before(HookType.Test)] +[Before(Test)] public void SetupTestArtifactDirectory() { - var testName = TestContext.Current!.TestDetails.TestName; + var testName = TestContext.Current!.Metadata.TestName; var sanitizedName = string.Concat(testName.Split(Path.GetInvalidFileNameChars())); var artifactDir = Path.Combine("test-artifacts", sanitizedName); Directory.CreateDirectory(artifactDir); @@ -270,7 +270,7 @@ public void MyTest() For large artifacts (videos, extensive logs), consider only attaching them when tests fail: ```csharp -[After(HookType.Test)] +[After(Test)] public async Task ConditionalArtifactAttachment() { var testContext = TestContext.Current; @@ -337,7 +337,7 @@ else ### Browser Testing with Playwright ```csharp -[After(HookType.Test)] +[After(Test)] public async Task CapturePlaywrightArtifacts() { var testContext = TestContext.Current; diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index 009164b524..4dc7b02c04 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -828,13 +828,13 @@ public class SharedDatabaseTests { private static DbContext _sharedContext; - [Before(HookType.Class)] + [Before(Class)] public static async Task ClassSetup() { _sharedContext = await SetupDatabaseAsync(); } - [Before(HookType.Test)] + [Before(Test)] public async Task TestSetup() { // Clear data between tests @@ -848,7 +848,7 @@ public class SharedDatabaseTests // Use _sharedContext } - [After(HookType.Class)] + [After(Class)] public static async Task ClassCleanup() { _sharedContext?.Dispose(); @@ -1062,7 +1062,7 @@ public class ConfigurationTests { private static IConfiguration _configuration; - [Before(HookType.Class)] + [Before(Class)] public static void SetupConfiguration() { _configuration = new ConfigurationBuilder() @@ -1091,7 +1091,7 @@ public class ConfigurationTests #### 1. Use Environment-Specific Files ```csharp -[Before(HookType.Class)] +[Before(Class)] public static void SetupConfiguration() { var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development"; @@ -1164,7 +1164,7 @@ dotnet user-secrets set "ConnectionStrings:Database" "Server=localhost;..." #### 3. Load Secrets in Tests ```csharp -[Before(HookType.Class)] +[Before(Class)] public static void SetupConfiguration() { _configuration = new ConfigurationBuilder() @@ -1186,7 +1186,7 @@ public static void SetupConfiguration() **Recommended for CI/CD:** ```csharp -[Before(HookType.Class)] +[Before(Class)] public static void SetupConfiguration() { _configuration = new ConfigurationBuilder() @@ -1244,7 +1244,7 @@ public class ConfigurationTests { private static AppSettings _settings; - [Before(HookType.Class)] + [Before(Class)] public static void SetupConfiguration() { var config = new ConfigurationBuilder() @@ -1272,7 +1272,7 @@ public class TestBase { protected static IConfiguration Configuration { get; private set; } - [Before(HookType.Assembly)] + [Before(Assembly)] public static void SetupSharedConfiguration() { Configuration = new ConfigurationBuilder() @@ -1332,7 +1332,7 @@ public class PerTestConfigTests **Debug:** ```csharp -[Before(HookType.Class)] +[Before(Class)] public static void SetupConfiguration() { var currentDir = Directory.GetCurrentDirectory(); @@ -1859,7 +1859,7 @@ public async Task FetchUserData() #### 1. Ensure Services Are Registered ```csharp // In your test setup or configuration -[Before(HookType.Assembly)] +[Before(Assembly)] public static void ConfigureServices() { var services = new ServiceCollection(); @@ -1980,7 +1980,7 @@ public class Conservative : IParallelLimit #### 3. Clear Test Data Between Runs ```csharp -[After(HookType.Test)] +[After(Test)] public void Cleanup() { GC.Collect(); // Force garbage collection if needed @@ -2002,11 +2002,11 @@ public void Cleanup() #### 1. Check Hook Scope ```csharp // ❌ Instance method for class-level hook -[Before(HookType.Class)] +[Before(Class)] public void ClassSetup() { } // Won't work! // ✅ Static method for class-level hook -[Before(HookType.Class)] +[Before(Class)] public static void ClassSetup() { } // Works! ``` From 12af71f3771c5229d69c38668edf99d08cd1b6ba Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:14:20 +0000 Subject: [PATCH 2/9] docs: merge 9 stub pages into parent docs, delete redundant files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merge properties.md into test-context.md as "Custom Properties" section - Merge class-constructors.md into dependency-injection.md with expanded content - Merge order.md into depends-on.md as "Test Ordering & Dependencies" - Combine and-conditions, or-conditions, scopes into combining-assertions.md - Delete combined-data-source-summary.md (redundant) - Merge executors.md STA example into extension-points.md - Delete examples/intro.md placeholder - Remove "Version History" from combined-data-source.md File count: 118 → 110 --- docs/docs/advanced/extension-points.md | 19 +++ docs/docs/assertions/and-conditions.md | 22 --- docs/docs/assertions/combining-assertions.md | 80 +++++++++++ docs/docs/assertions/or-conditions.md | 27 ---- docs/docs/assertions/scopes.md | 42 ------ docs/docs/examples/intro.md | 5 - docs/docs/execution/executors.md | 42 ------ .../combined-data-source-summary.md | 126 ------------------ .../test-authoring/combined-data-source.md | 7 - docs/docs/test-authoring/depends-on.md | 31 ++++- docs/docs/test-authoring/order.md | 39 ------ .../docs/test-lifecycle/class-constructors.md | 10 -- .../test-lifecycle/dependency-injection.md | 80 ++++++++--- docs/docs/test-lifecycle/properties.md | 25 ---- docs/docs/test-lifecycle/test-context.md | 25 ++++ 15 files changed, 217 insertions(+), 363 deletions(-) delete mode 100644 docs/docs/assertions/and-conditions.md create mode 100644 docs/docs/assertions/combining-assertions.md delete mode 100644 docs/docs/assertions/or-conditions.md delete mode 100644 docs/docs/assertions/scopes.md delete mode 100644 docs/docs/examples/intro.md delete mode 100644 docs/docs/execution/executors.md delete mode 100644 docs/docs/test-authoring/combined-data-source-summary.md delete mode 100644 docs/docs/test-authoring/order.md delete mode 100644 docs/docs/test-lifecycle/class-constructors.md delete mode 100644 docs/docs/test-lifecycle/properties.md diff --git a/docs/docs/advanced/extension-points.md b/docs/docs/advanced/extension-points.md index d853d5431c..39980c6aac 100644 --- a/docs/docs/advanced/extension-points.md +++ b/docs/docs/advanced/extension-points.md @@ -75,6 +75,25 @@ public async Task MyTest() } ``` +### STA Thread Example + +A common use case for `ITestExecutor` is running tests on an STA thread (required by some COM / UI components on Windows): + +```csharp +[Test] +[TestExecutor] +public async Task With_STA() +{ + await Assert.That(Thread.CurrentThread.GetApartmentState()).IsEqualTo(ApartmentState.STA); +} + +[Test] +public async Task Without_STA() +{ + await Assert.That(Thread.CurrentThread.GetApartmentState()).IsEqualTo(ApartmentState.MTA); +} +``` + ## IHookExecutor The `IHookExecutor` interface allows you to customize how setup and cleanup hooks are executed. This is useful for: diff --git a/docs/docs/assertions/and-conditions.md b/docs/docs/assertions/and-conditions.md deleted file mode 100644 index 8f22a5a63c..0000000000 --- a/docs/docs/assertions/and-conditions.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -sidebar_position: 3 ---- - -# And Conditions - -TUnit can chain assertions together, using the `And` property. This reads very much like English, and aims to keep the test easy to read and understand, and doesn't require you repeat boilerplate code such as `Assert.That` over and over. - -Every condition must pass when using `And`s: - -```csharp - [Test] - public async Task MyTest() - { - var result = Add(1, 2); - - await Assert.That(result) - .IsNotNull() - .And.IsPositive() - .And.IsEqualTo(3); - } -``` \ No newline at end of file diff --git a/docs/docs/assertions/combining-assertions.md b/docs/docs/assertions/combining-assertions.md new file mode 100644 index 0000000000..766daad168 --- /dev/null +++ b/docs/docs/assertions/combining-assertions.md @@ -0,0 +1,80 @@ +--- +sidebar_position: 11 +--- + +# Combining Assertions + +TUnit provides several ways to combine multiple assertions within a single test: chaining with `.And` and `.Or`, and grouping with `Assert.Multiple()`. + +## And Conditions + +Use the `.And` property to chain multiple conditions on the same value. Every condition must pass for the assertion to succeed. This reads naturally and avoids repeating `Assert.That(...)` for each check. + +```csharp +[Test] +public async Task MyTest() +{ + var result = Add(1, 2); + + await Assert.That(result) + .IsNotNull() + .And.IsPositive() + .And.IsEqualTo(3); +} +``` + +## Or Conditions + +Use the `.Or` property when at least one condition must pass. This is useful for values that are valid across a known set of outcomes. + +```csharp +[Test] +public async Task MyTest() +{ + var result = ComputeValue(); + + await Assert.That(result) + .IsEqualTo(2) + .Or.IsEqualTo(3) + .Or.IsEqualTo(4); +} +``` + +`.And` and `.Or` can be mixed in a single chain. Conditions are evaluated left-to-right. + +## Assertion Scopes + +By default, a failing assertion throws immediately and stops the test. `Assert.Multiple()` creates a scope that collects all failures and reports them together when the scope exits. This is useful for asserting multiple properties on an object without the fix-one-rerun-fix-another cycle. + +Implicit scope (covers the rest of the method): + +```csharp +[Test] +public async Task MyTest() +{ + var result = Add(1, 2); + + using var _ = Assert.Multiple(); + + await Assert.That(result).IsPositive(); + await Assert.That(result).IsEqualTo(3); +} +``` + +Explicit scope (covers only the block): + +```csharp +[Test] +public async Task MyTest() +{ + var result = Add(1, 2); + + using (Assert.Multiple()) + { + await Assert.That(result).IsPositive(); + await Assert.That(result).IsEqualTo(3); + } +} +``` + +Both forms aggregate failures. When the `using` scope ends, any accumulated assertion failures are thrown as a single exception listing all violations. diff --git a/docs/docs/assertions/or-conditions.md b/docs/docs/assertions/or-conditions.md deleted file mode 100644 index 98c5f41345..0000000000 --- a/docs/docs/assertions/or-conditions.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Or Conditions - -Similar to the `And` property, there is also the `Or` property. - -When using this, only one condition needs to pass: - -```csharp - [Test] - [Repeat(100)] - public async Task MyTest() - { - int[] array = [1, 2]; - var randomValue1 = Random.Shared.GetItems(array, 1).First(); - var randomValue2 = Random.Shared.GetItems(array, 1).First(); - - var result = Add(randomValue1, randomValue2); - - await Assert.That(result) - .IsEqualTo(2) - .Or.IsEqualTo(3) - .Or.IsEqualTo(4); - } -``` \ No newline at end of file diff --git a/docs/docs/assertions/scopes.md b/docs/docs/assertions/scopes.md deleted file mode 100644 index 024d64438c..0000000000 --- a/docs/docs/assertions/scopes.md +++ /dev/null @@ -1,42 +0,0 @@ -# Assertion Scopes - -In TUnit you can create an assertion scope by calling `Assert.Multiple()`. This returns an `IDisposable` and so you should use that by encapsulating the returned value in a `using` block. This will make sure that any assertion exceptions are aggregated together and thrown only after the scope is exited. - -This is useful for asserting multiple properties and showing all errors at once, instead of having to fix > rerun > fix > rerun. - -## Behavior Differences - -**Outside `Assert.Multiple()`**: Assertions throw immediately when they fail, stopping test execution. - -**Inside `Assert.Multiple()`**: Assertions accumulate failures and only throw when the scope exits, allowing all assertions within the scope to execute. - -Implicit Scope: - -```csharp -[Test] - public async Task MyTest() - { - var result = Add(1, 2); - - using var _ = Assert.Multiple(); - - await Assert.That(result).IsPositive(); - await Assert.That(result).IsEqualTo(3); - } -``` - -Explicit Scope: - -```csharp -[Test] - public async Task MyTest() - { - var result = Add(1, 2); - - using (Assert.Multiple()) - { - await Assert.That(result).IsPositive(); - await Assert.That(result).IsEqualTo(3); - } - } -``` diff --git a/docs/docs/examples/intro.md b/docs/docs/examples/intro.md deleted file mode 100644 index e5fcae3dd1..0000000000 --- a/docs/docs/examples/intro.md +++ /dev/null @@ -1,5 +0,0 @@ -# Examples - -This can serve as a place to show how to use TUnit to test more complex systems, utilising advanced features like ClassData sources with IAsyncInitializers and IAsyncDisposables, or utilising test events to drive things. - -As tests come in all shapes and sizes, this is a good place for community contributions. If you have a good example for showing other users how to setup a specific testing scenario, then please submit a pull request with code examples. diff --git a/docs/docs/execution/executors.md b/docs/docs/execution/executors.md deleted file mode 100644 index dea36d63fb..0000000000 --- a/docs/docs/execution/executors.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -sidebar_position: 12 ---- - -# Executors - -In some advanced cases, you may need to control how a test or hook is executed. -There are two interfaces that you can implement to control this: -- `IHookExecutor` -- `ITestExecutor` - -You will be given a delegate, and some context about what is executing, and you can control how to invoke it. - -To register your executor, on your test/hook, you can place an attribute of either: -- `[HookExecutor<...>]` -- `[TestExecutor<...>]` - -An example of where you might need this is running in a STA Thread. - -Here's an example: - -```csharp -using TUnit.Core; - -namespace MyTestProject; - -public class MyTestClass -{ - [Test] - [TestExecutor] - public async Task With_STA() - { - await Assert.That(Thread.CurrentThread.GetApartmentState()).IsEqualTo(ApartmentState.STA); - } - - [Test] - public async Task Without_STA() - { - await Assert.That(Thread.CurrentThread.GetApartmentState()).IsEqualTo(ApartmentState.MTA); - } -} -``` \ No newline at end of file diff --git a/docs/docs/test-authoring/combined-data-source-summary.md b/docs/docs/test-authoring/combined-data-source-summary.md deleted file mode 100644 index fec77d7ef4..0000000000 --- a/docs/docs/test-authoring/combined-data-source-summary.md +++ /dev/null @@ -1,126 +0,0 @@ -# CombinedDataSources - Quick Reference - -## What is it? - -`[CombinedDataSources]` allows you to apply different data source attributes to individual test method parameters, automatically generating all possible combinations (Cartesian product). - -## Quick Start - -```csharp -[Test] -[CombinedDataSources] -public async Task MyTest( - [Arguments(1, 2, 3)] int x, - [MethodDataSource(nameof(GetStrings))] string y) -{ - // Automatically creates 3 × 2 = 6 test cases - await Assert.That(x).IsIn([1, 2, 3]); - await Assert.That(y).IsIn(["Hello", "World"]); -} - -public static IEnumerable GetStrings() -{ - yield return "Hello"; - yield return "World"; -} -``` - -## Key Benefits - -✅ **Maximum Flexibility** - Mix ANY data source types on different parameters -✅ **Automatic Combinations** - Generates Cartesian product automatically -✅ **Clean Syntax** - Data sources defined right on the parameters -✅ **Type Safe** - Full compile-time type checking -✅ **AOT Compatible** - Works with Native AOT compilation - -## Supported Data Sources - -Apply these to individual parameters: - -- `[Arguments(1, 2, 3)]` - Inline values -- `[MethodDataSource(nameof(Method))]` - From method -- `[ClassDataSource]` - Generate instances -- `[CustomDataSource]` - Any `IDataSourceAttribute` - -## Cartesian Product - -With 3 parameters: -- Parameter A: 2 values -- Parameter B: 3 values -- Parameter C: 4 values - -**Result**: 2 × 3 × 4 = **24 test cases** - -## Common Patterns - -### Pattern 1: All Arguments -```csharp -[Test] -[CombinedDataSources] -public void Test( - [Arguments(1, 2)] int a, - [Arguments("x", "y")] string b) -{ - // 2 × 2 = 4 tests -} -``` - -### Pattern 2: Mixed Sources -```csharp -[Test] -[CombinedDataSources] -public void Test( - [Arguments(1, 2)] int a, - [MethodDataSource(nameof(GetData))] string b, - [ClassDataSource] MyClass c) -{ - // 2 × N × 1 = 2N tests -} -``` - -### Pattern 3: Multiple Per Parameter -```csharp -[Test] -[CombinedDataSources] -public void Test( - [Arguments(1, 2)] - [Arguments(3, 4)] int a, // Combines to 4 values - [Arguments("x")] string b) -{ - // 4 × 1 = 4 tests -} -``` - -## When to Use - -✅ **Use CombinedDataSources when:** -- Different parameters need different data sources -- You want maximum flexibility in data generation -- You need to test all combinations of inputs - -❌ **Use alternatives when:** -- All parameters use same type of data source → Consider `[Matrix]` -- You only need specific combinations → Use multiple `[Test]` methods with `[Arguments]` -- Test count would be excessive → Break into smaller tests - -## Performance Warning - -⚠️ **Be mindful of exponential growth!** - -| Params | Values Each | Total Tests | -|--------|-------------|-------------| -| 2 | 3 | 9 | -| 3 | 3 | 27 | -| 4 | 3 | 81 | -| 5 | 3 | 243 | -| 3 | 10 | 1,000 | -| 4 | 10 | 10,000 | - -## Full Documentation - -See [CombinedDataSources](combined-data-source.md) for complete documentation including: -- Advanced scenarios -- Error handling -- AOT compilation details -- Troubleshooting guide -- Real-world examples diff --git a/docs/docs/test-authoring/combined-data-source.md b/docs/docs/test-authoring/combined-data-source.md index 40fa2985ff..0d0c018ee0 100644 --- a/docs/docs/test-authoring/combined-data-source.md +++ b/docs/docs/test-authoring/combined-data-source.md @@ -434,10 +434,3 @@ public async Task Database_Pagination( - [ClassDataSource Documentation](class-data-source.md) - [Arguments Attribute Documentation](arguments.md) -## Version History - -- **v1.0.0** - Initial release - - Parameter-level data source support - - Cartesian product generation - - Support for all `IDataSourceAttribute` implementations - - Full AOT compatibility diff --git a/docs/docs/test-authoring/depends-on.md b/docs/docs/test-authoring/depends-on.md index c192670fcb..48f68b1ec0 100644 --- a/docs/docs/test-authoring/depends-on.md +++ b/docs/docs/test-authoring/depends-on.md @@ -1,4 +1,33 @@ -# Depends On +# Test Ordering & Dependencies + +## Ordering with [Order] + +:::warning + +It is recommended to use `[DependsOn]` instead of `[Order]` as it provides more flexibility and does not sacrifice parallelisation. + +::: + +By default, TUnit tests run in parallel, so ordering has no effect unless parallelism is disabled. To order tests that share a `[NotInParallel]` constraint, set the `Order` property. Tests execute from smallest to largest order value. + +```csharp +public class MyTestClass +{ + [Test] + [NotInParallel(Order = 1)] + public async Task Step1_CreateRecord() + { + } + + [Test] + [NotInParallel(Order = 2)] + public async Task Step2_VerifyRecord() + { + } +} +``` + +## Dependencies with [DependsOn] :::warning Test Isolation Best Practice **Important**: Tests should ideally be self-contained, isolated, and side-effect free. This ensures they are: diff --git a/docs/docs/test-authoring/order.md b/docs/docs/test-authoring/order.md deleted file mode 100644 index 8cb252d595..0000000000 --- a/docs/docs/test-authoring/order.md +++ /dev/null @@ -1,39 +0,0 @@ -# Ordering Tests - -:::warning - -It is recommended to use [DependsOn(...)] as it provides more flexibility and doesn't sacrifice parallelisation. - -::: - -By default, TUnit tests will run in parallel. This means there is no order and it doesn't make sense to be able to control that. - -However, if tests aren't running in parallel, they can absolutely be ordered, and this is necessary for some systems. - -To control ordering, there is an `Order` property on the `[NotInParallel]` attribute. - -Orders will execute from smallest to largest. So 1 first, then 2, then 3, etc. - -```csharp -using TUnit.Core; - -namespace MyTestProject; - -public class MyTestClass -{ - [Test] - [NotInParallel(Order = 1)] - public async Task MyTest() - { - - } - - [Test] - [NotInParallel(Order = 2)] - public async Task MyTest2() - { - - } -} -``` - diff --git a/docs/docs/test-lifecycle/class-constructors.md b/docs/docs/test-lifecycle/class-constructors.md deleted file mode 100644 index 802479e473..0000000000 --- a/docs/docs/test-lifecycle/class-constructors.md +++ /dev/null @@ -1,10 +0,0 @@ -# Class Constructor Helpers - -Some test suites might be more complex than others, and a user may want control over 'newing' up their test classes. -This control is given to you by the `[ClassConstructorAttribute]` - Where `T` is a class that implements `IClassConstructor`. - -This interface simply requires you to generate a `T` object - How you do that is up to you! - -You can also add [event-subscribing interfaces](test-lifecycle/event-subscribing.md) to get notified for things like when the test has finished. This functionality can be used to dispose objects afterwards, etc. - -These attributes are new'd up per test, so you can store state within them. diff --git a/docs/docs/test-lifecycle/dependency-injection.md b/docs/docs/test-lifecycle/dependency-injection.md index f46516a924..4731ca3396 100644 --- a/docs/docs/test-lifecycle/dependency-injection.md +++ b/docs/docs/test-lifecycle/dependency-injection.md @@ -1,21 +1,50 @@ # Dependency Injection -Dependency Injection can be set up by leveraging the power of the Data Source Generators. +TUnit provides two mechanisms for controlling how test classes are constructed: the low-level `IClassConstructor` interface and the higher-level `DependencyInjectionDataSourceAttribute` helper. Both are registered via attributes and give full control over how constructor arguments are resolved. -TUnit provides you an abstract class to handle most of the logic for you, you need to simply provide the implementation on how to create a DI Scope, and then how to get or create an object when given its type. +## IClassConstructor -So create a new class that inherits from `DependencyInjectionDataSourceAttribute` and pass through the Scope type as the generic argument. +The `IClassConstructor` interface gives direct control over how test class instances are created. Implement this interface when you need custom instantiation logic — for example, resolving dependencies from a lightweight container, applying decorators, or wrapping construction in a factory. -Here's an example of that using the Microsoft.Extensions.DependencyInjection library: +Register it with `[ClassConstructor]` on the test class. Each test gets its own attribute instance, so you can safely store per-test state. ```csharp -using TUnit.Core; +public class CustomConstructor : IClassConstructor +{ + public T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>( + ClassConstructorMetadata classConstructorMetadata) where T : class + { + // Resolve T however you like — manual construction, a container, etc. + return Activator.CreateInstance(); + } +} + +[ClassConstructor] +public class MyTestClass(SomeDependency dep) +{ + [Test] + public async Task MyTest() + { + // dep was provided by CustomConstructor.Create() + } +} +``` + +The `ClassConstructorMetadata` parameter provides context about the test being constructed, including the test's data-source arguments and metadata. You can also implement [event-subscribing interfaces](test-lifecycle/event-subscribing.md) on the same class to get notified when a test finishes — useful for disposing objects after the test completes. -namespace MyTestProject; +## DependencyInjectionDataSourceAttribute -public class MicrosoftDependencyInjectionDataSourceAttribute : DependencyInjectionDataSourceAttribute +For DI-container integration, TUnit provides `DependencyInjectionDataSourceAttribute` — an abstract base class that handles scope lifecycle automatically. You supply two methods: + +1. **`CreateScope`** — create a DI scope (called once per test class instance) +2. **`Create`** — resolve a service from that scope by type + +### Microsoft.Extensions.DependencyInjection Example + +```csharp +public class MicrosoftDIAttribute : DependencyInjectionDataSourceAttribute { - private static readonly IServiceProvider ServiceProvider = CreateSharedServiceProvider(); + private static readonly IServiceProvider ServiceProvider = BuildProvider(); public override IServiceScope CreateScope(DataGeneratorMetadata dataGeneratorMetadata) { @@ -26,25 +55,42 @@ public class MicrosoftDependencyInjectionDataSourceAttribute : DependencyInjecti { return scope.ServiceProvider.GetService(type); } - - private static IServiceProvider CreateSharedServiceProvider() + + private static IServiceProvider BuildProvider() { return new ServiceCollection() - .AddSingleton() - .AddSingleton() - .AddTransient() + .AddSingleton() + .AddTransient() .BuildServiceProvider(); } } +``` -[MicrosoftDependencyInjectionDataSource] -public class MyTestClass(SomeClass1 someClass1, SomeClass2 someClass2, SomeClass3 someClass3) +Apply the attribute to a test class that accepts constructor parameters. TUnit resolves each parameter through the `Create` method: + +```csharp +[MicrosoftDI] +public class UserServiceTests(IUserRepository repo, IEmailService email) { [Test] - public async Task Test() + public async Task CreateUser_SendsWelcomeEmail() { - // ... + var service = new UserService(repo, email); + await service.CreateAsync("alice@example.com"); + + await Assert.That(((FakeEmailService)email).SentCount).IsEqualTo(1); } } ``` +### Other Containers + +The same pattern works with any DI container. Replace `IServiceScope` with whatever scope type the container uses (e.g., `ILifetimeScope` for Autofac) and implement `CreateScope` / `Create` accordingly. + +## Choosing Between the Two + +| Need | Use | +|------|-----| +| Full DI container with scoped lifetimes | `DependencyInjectionDataSourceAttribute` | +| Simple manual construction or lightweight container | `IClassConstructor` | +| Disposal / cleanup after tests | Either — implement `IAsyncDisposable` on the scope or use event-subscribing interfaces on the constructor | diff --git a/docs/docs/test-lifecycle/properties.md b/docs/docs/test-lifecycle/properties.md deleted file mode 100644 index 68f20e3637..0000000000 --- a/docs/docs/test-lifecycle/properties.md +++ /dev/null @@ -1,25 +0,0 @@ -# Properties - -Custom properties can be added to a test using the `[PropertyAttribute]`. - -Custom properties can be used for test filtering: `dotnet run --treenode-filter /*/*/*/*[PropertyName=PropertyValue]` - -Custom properties can also be viewed in the `TestContext` - Which can be used in logic during setups or cleanups. - -This can be used on base classes and inherited to affect all tests in sub-classes. - -```csharp -using TUnit.Core; - -namespace MyTestProject; - -public class MyTestClass -{ - [Test] - [Property("PropertyName", "PropertyValue")] - public async Task MyTest(CancellationToken cancellationToken) - { - - } -} -``` diff --git a/docs/docs/test-lifecycle/test-context.md b/docs/docs/test-lifecycle/test-context.md index 825c0ef298..d17ba47bb2 100644 --- a/docs/docs/test-lifecycle/test-context.md +++ b/docs/docs/test-lifecycle/test-context.md @@ -80,6 +80,31 @@ These are useful for any test that needs unique resource names — database tabl If you're using `TUnit.AspNetCore`, the `WebApplicationTest` base class provides the same helpers as `protected` methods (`GetIsolatedName`, `GetIsolatedPrefix`). Both share the same underlying counter, so IDs are unique across all test types. ::: +## Custom Properties + +Custom properties can be added to a test using the `[Property]` attribute. Properties are key-value pairs of strings that serve multiple purposes: + +- **Test filtering**: Filter tests at the command line with `dotnet run --treenode-filter /*/*/*/*[PropertyName=PropertyValue]` +- **Runtime logic**: Access properties in setup/cleanup hooks via `TestContext` to conditionally execute logic +- **Inheritance**: Apply `[Property]` on a base class and all sub-class tests inherit it + +```csharp +public class MyTestClass +{ + [Test] + [Property("Category", "Integration")] + public async Task MyTest(CancellationToken cancellationToken) + { + // Access the property at runtime + var properties = TestContext.Current!.Metadata.TestDetails.CustomProperties; + if (properties.ContainsKey("Category")) + { + // Conditional logic based on property + } + } +} +``` + ## Dependency Injection **Note**: `TestContext` does NOT provide direct access to dependency injection services. The internal service provider in `TestContext` is exclusively for TUnit framework services and is not meant for user-provided dependencies. From 350b6607cdf32c4d43307e2e884d06f3aa66874d Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:19:34 +0000 Subject: [PATCH 3/9] docs: extract shared content to eliminate duplication - Create extensions/code-coverage.md as single authoritative coverage page - Remove ~600-word duplicated coverage sections from xunit.md, nunit.md, mstest.md, and extensions.md (replaced with links) - Deduplicate cleanup.md by removing copy-pasted hook parameter sections from setup.md (replaced with cross-reference) - Consolidate SharedType explanations: class-data-source.md now links to property-injection.md as authoritative source - Remove weak BeforeEvery/AfterEvery entries that just said "same as" 647 lines of duplication removed --- docs/docs/extensions/code-coverage.md | 131 ++++++++++++++ docs/docs/extensions/extensions.md | 77 +------- docs/docs/migration/mstest.md | 169 +----------------- docs/docs/migration/nunit.md | 169 +----------------- docs/docs/migration/xunit.md | 169 +----------------- docs/docs/test-authoring/class-data-source.md | 24 +-- docs/docs/test-lifecycle/cleanup.md | 49 +---- docs/docs/test-lifecycle/setup.md | 6 - 8 files changed, 147 insertions(+), 647 deletions(-) create mode 100644 docs/docs/extensions/code-coverage.md diff --git a/docs/docs/extensions/code-coverage.md b/docs/docs/extensions/code-coverage.md new file mode 100644 index 0000000000..63f7af0306 --- /dev/null +++ b/docs/docs/extensions/code-coverage.md @@ -0,0 +1,131 @@ +--- +sidebar_position: 2 +title: Code Coverage +--- + +# Code Coverage + +TUnit includes built-in code coverage support via `Microsoft.Testing.Extensions.CodeCoverage`, which is automatically included when you install the **TUnit** meta package. No additional packages are required. + +## Coverlet is Not Compatible + +If you are migrating from another test framework and currently use **Coverlet** (`coverlet.collector` or `coverlet.msbuild`), you must remove it. TUnit uses Microsoft.Testing.Platform, not VSTest, and Coverlet only works with the legacy VSTest platform. + +Remove these from your `.csproj`: + +```xml + + + +``` + +## Running Coverage + +Use the `--coverage` flag when running your tests: + +```bash +# Basic coverage collection +dotnet run --configuration Release --coverage + +# Specify output location +dotnet run --configuration Release --coverage --coverage-output ./coverage/ + +# Specify output format (cobertura is the default) +dotnet run --configuration Release --coverage --coverage-output-format cobertura + +# Multiple formats +dotnet run --configuration Release --coverage \ + --coverage-output-format cobertura \ + --coverage-output-format xml +``` + +## Output Formats + +The Microsoft coverage tool supports multiple output formats: + +| Format | Flag | Notes | +|--------|------|-------| +| Cobertura | `--coverage-output-format cobertura` | Default. Widely supported by CI tools. | +| XML | `--coverage-output-format xml` | Visual Studio format. | + +You can specify multiple `--coverage-output-format` flags to generate several formats in one run. + +## Viewing Coverage Results + +Coverage files are generated in your test output directory: + +``` +TestResults/ + ├── coverage.cobertura.xml + └── / + └── coverage.xml +``` + +Tools for viewing results: +- **Visual Studio** -- Built-in coverage viewer +- **VS Code** -- Extensions like "Coverage Gutters" +- **ReportGenerator** -- Generate HTML reports: `reportgenerator -reports:coverage.cobertura.xml -targetdir:coveragereport` +- **CI Tools** -- Most CI systems can parse Cobertura format natively + +## Configuration + +### testconfig.json + +You can customize coverage behavior with a `testconfig.json` file placed in the same directory as your test project: + +```json +{ + "codeCoverage": { + "Configuration": { + "CodeCoverage": { + "ModulePaths": { + "Include": [".*\\.dll$"], + "Exclude": [".*tests\\.dll$"] + } + } + } + } +} +``` + +The file is picked up automatically when running tests. + +### XML Settings File + +Alternatively, you can use an XML coverage settings file: + +```bash +dotnet run --configuration Release --coverage --coverage-settings coverage.config +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +- name: Run tests with coverage + run: dotnet run --project ./tests/MyProject.Tests --configuration Release --coverage +``` + +### Azure Pipelines + +```yaml +- task: DotNetCoreCLI@2 + inputs: + command: 'run' + arguments: '--configuration Release --coverage --coverage-output $(Agent.TempDirectory)/coverage/' +``` + +## Troubleshooting + +**Coverage files not generated?** +- Ensure you are using the `TUnit` meta package, not just `TUnit.Engine`. +- Verify you have a recent .NET SDK installed. + +**Missing coverage for some assemblies?** +- Use a `testconfig.json` file to explicitly include or exclude modules. +- See [Microsoft's configuration documentation](https://github.com/microsoft/codecoverage/blob/main/docs/configuration.md). + +**Further reading:** +- [Microsoft Code Coverage extension docs](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-platform-extensions-code-coverage) +- [Unit Testing Code Coverage Guide](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage) diff --git a/docs/docs/extensions/extensions.md b/docs/docs/extensions/extensions.md index 33a3805265..bba6f53af3 100644 --- a/docs/docs/extensions/extensions.md +++ b/docs/docs/extensions/extensions.md @@ -22,82 +22,9 @@ If you don't want these extensions, you can reference `TUnit.Engine` and `TUnit. ### Code Coverage -Code coverage is provided via the `Microsoft.Testing.Extensions.CodeCoverage` NuGet package. +Code coverage is provided via `Microsoft.Testing.Extensions.CodeCoverage`, included automatically with the TUnit package. -**✅ Included automatically with the TUnit package** - No manual installation needed! - -#### Usage - -Run your tests with the `--coverage` flag: -```bash -# Basic coverage -dotnet run --configuration Release --coverage - -# Specify output location -dotnet run --configuration Release --coverage --coverage-output ./coverage/ - -# Specify output format (cobertura is default) -dotnet run --configuration Release --coverage --coverage-output-format cobertura - -# Multiple formats -dotnet run --configuration Release --coverage \ - --coverage-output-format cobertura \ - --coverage-output-format xml -``` - -#### Important: Coverlet Incompatibility ⚠️ - -**If you're migrating from xUnit, NUnit, or MSTest:** - -- **Remove Coverlet** (`coverlet.collector` or `coverlet.msbuild`) from your project -- TUnit uses Microsoft.Testing.Platform (not VSTest), which is incompatible with Coverlet -- Microsoft.Testing.Extensions.CodeCoverage is the modern replacement and provides the same functionality - -**Migration Example:** -```xml - - - - - - -``` - -See the migration guides for detailed instructions: -- [xUnit Migration Guide - Code Coverage](../migration/xunit.md#code-coverage) -- [NUnit Migration Guide - Code Coverage](../migration/nunit.md#code-coverage) -- [MSTest Migration Guide - Code Coverage](../migration/mstest.md#code-coverage) - -#### Advanced Configuration - -You can customize coverage with a `testconfig.json` file: - -**testconfig.json:** -```json -{ - "codeCoverage": { - "Configuration": { - "CodeCoverage": { - "ModulePaths": { - "Include": [".*\\.dll$"], - "Exclude": [".*tests\\.dll$"] - } - } - } - } -} -``` - -Place the `testconfig.json` file in the same directory as your test project. It will be picked up automatically when running tests. - -**Alternatively, you can use an XML coverage settings file:** -```bash -dotnet run --configuration Release --coverage --coverage-settings coverage.config -``` - -**📚 More Resources:** -- [Microsoft's Code Coverage Documentation](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-platform-extensions-code-coverage) -- [Unit Testing Code Coverage Guide](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage) +See the [Code Coverage](code-coverage.md) page for usage, configuration, and CI/CD integration. --- diff --git a/docs/docs/migration/mstest.md b/docs/docs/migration/mstest.md index cb829df2a4..6000f9038c 100644 --- a/docs/docs/migration/mstest.md +++ b/docs/docs/migration/mstest.md @@ -1122,171 +1122,6 @@ public class ContextTests ## Code Coverage -### Important: Coverlet is Not Compatible with TUnit +TUnit includes built-in code coverage support. Do **not** use Coverlet — it is incompatible with TUnit's Microsoft.Testing.Platform. -If you're using **Coverlet** (`coverlet.collector` or `coverlet.msbuild`) for code coverage in your MSTest projects, you'll need to migrate to **Microsoft.Testing.Extensions.CodeCoverage**. - -**Why?** TUnit uses the modern `Microsoft.Testing.Platform` instead of VSTest, and Coverlet only works with the legacy VSTest platform. - -### Good News: Coverage is Built In! 🎉 - -When you install the **TUnit** meta package, it automatically includes `Microsoft.Testing.Extensions.CodeCoverage` for you. You don't need to install it separately! - -### Migration Steps - -#### 1. Remove Coverlet Packages - -Remove any Coverlet packages from your project file: - -**Remove these lines from your `.csproj`:** -```xml - - - -``` - -#### 2. Verify TUnit Meta Package - -Ensure you're using the **TUnit** meta package (not just TUnit.Core): - -**Your `.csproj` should have:** -```xml - -``` - -This automatically brings in: -- `Microsoft.Testing.Extensions.CodeCoverage` (coverage support) -- `Microsoft.Testing.Extensions.TrxReport` (test result reports) - -#### 3. Update Your Coverage Commands - -Replace your old Coverlet commands with the new Microsoft coverage syntax: - -**Old (Coverlet with MSTest):** -```bash -# With coverlet.collector -dotnet test --collect:"XPlat Code Coverage" - -# With coverlet.msbuild -dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura -``` - -**New (TUnit with Microsoft Coverage):** -```bash -# Run tests with coverage -dotnet run --configuration Release --coverage - -# Specify output location -dotnet run --configuration Release --coverage --coverage-output ./coverage/ - -# Specify coverage format (default is cobertura) -dotnet run --configuration Release --coverage --coverage-output-format cobertura - -# Multiple formats -dotnet run --configuration Release --coverage --coverage-output-format cobertura --coverage-output-format xml -``` - -#### 4. Update CI/CD Pipelines - -If you have CI/CD pipelines that reference Coverlet, update them to use the new commands: - -**GitHub Actions Example:** -```yaml -# Old (MSTest with Coverlet) -- name: Run tests with coverage - run: dotnet test --collect:"XPlat Code Coverage" - -# New (TUnit with Microsoft Coverage) -- name: Run tests with coverage - run: dotnet run --project ./tests/MyProject.Tests --configuration Release --coverage -``` - -**Azure Pipelines Example:** -```yaml -# Old (MSTest with Coverlet) -- task: DotNetCoreCLI@2 - inputs: - command: 'test' - arguments: '--collect:"XPlat Code Coverage"' - -# New (TUnit with Microsoft Coverage) -- task: DotNetCoreCLI@2 - inputs: - command: 'run' - arguments: '--configuration Release --coverage --coverage-output $(Agent.TempDirectory)/coverage/' -``` - -### Coverage Output Formats - -The Microsoft coverage tool supports multiple output formats: - -```bash -# Cobertura (default, widely supported) -dotnet run --configuration Release --coverage --coverage-output-format cobertura - -# XML (Visual Studio format) -dotnet run --configuration Release --coverage --coverage-output-format xml - -# Cobertura + XML -dotnet run --configuration Release --coverage \ - --coverage-output-format cobertura \ - --coverage-output-format xml -``` - -### Viewing Coverage Results - -Coverage files are generated in your test output directory: - -``` -TestResults/ - ├── coverage.cobertura.xml - └── / - └── coverage.xml -``` - -You can view these with: -- **Visual Studio** - Built-in coverage viewer (MSTest users will find this familiar) -- **VS Code** - Extensions like "Coverage Gutters" -- **ReportGenerator** - Generate HTML reports: `reportgenerator -reports:coverage.cobertura.xml -targetdir:coveragereport` -- **CI Tools** - Most CI systems can parse Cobertura format natively (same as before) - -### Advanced Coverage Configuration - -You can customize coverage behavior with a `testconfig.json` file: - -**testconfig.json:** -```json -{ - "codeCoverage": { - "Configuration": { - "CodeCoverage": { - "ModulePaths": { - "Include": [".*\\.dll$"], - "Exclude": [".*tests\\.dll$"] - } - } - } - } -} -``` - -Place the `testconfig.json` file in the same directory as your test project. It will be picked up automatically when running tests. - -**Alternatively, you can use an XML coverage settings file:** -```bash -dotnet run --configuration Release --coverage --coverage-settings coverage.config -``` - -### Troubleshooting - -**Coverage files not generated?** -- Ensure you're using the TUnit meta package, not just TUnit.Engine -- Verify you have a recent .NET SDK installed - -**Missing coverage for some assemblies?** -- Use a `testconfig.json` file to explicitly include/exclude modules -- See [Microsoft's documentation](https://github.com/microsoft/codecoverage/blob/main/docs/configuration.md) - -**Need help?** -- See [TUnit Code Coverage Documentation](../extensions/extensions.md#code-coverage) -- Check [Microsoft's Code Coverage Guide](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage) \ No newline at end of file +See the [Code Coverage guide](../extensions/code-coverage.md) for setup and configuration. \ No newline at end of file diff --git a/docs/docs/migration/nunit.md b/docs/docs/migration/nunit.md index e5d77f08ef..1dcedea5cd 100644 --- a/docs/docs/migration/nunit.md +++ b/docs/docs/migration/nunit.md @@ -838,171 +838,6 @@ public static class AssemblyHooks ## Code Coverage -### Important: Coverlet is Not Compatible with TUnit +TUnit includes built-in code coverage support. Do **not** use Coverlet — it is incompatible with TUnit's Microsoft.Testing.Platform. -If you're using **Coverlet** (`coverlet.collector` or `coverlet.msbuild`) for code coverage in your NUnit projects, you'll need to migrate to **Microsoft.Testing.Extensions.CodeCoverage**. - -**Why?** TUnit uses the modern `Microsoft.Testing.Platform` instead of VSTest, and Coverlet only works with the legacy VSTest platform. - -### Good News: Coverage is Built In! 🎉 - -When you install the **TUnit** meta package, it automatically includes `Microsoft.Testing.Extensions.CodeCoverage` for you. You don't need to install it separately! - -### Migration Steps - -#### 1. Remove Coverlet Packages - -Remove any Coverlet packages from your project file: - -**Remove these lines from your `.csproj`:** -```xml - - - -``` - -#### 2. Verify TUnit Meta Package - -Ensure you're using the **TUnit** meta package (not just TUnit.Core): - -**Your `.csproj` should have:** -```xml - -``` - -This automatically brings in: -- `Microsoft.Testing.Extensions.CodeCoverage` (coverage support) -- `Microsoft.Testing.Extensions.TrxReport` (test result reports) - -#### 3. Update Your Coverage Commands - -Replace your old Coverlet commands with the new Microsoft coverage syntax: - -**Old (Coverlet with NUnit):** -```bash -# With coverlet.collector -dotnet test --collect:"XPlat Code Coverage" - -# With coverlet.msbuild -dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura -``` - -**New (TUnit with Microsoft Coverage):** -```bash -# Run tests with coverage -dotnet run --configuration Release --coverage - -# Specify output location -dotnet run --configuration Release --coverage --coverage-output ./coverage/ - -# Specify coverage format (default is cobertura) -dotnet run --configuration Release --coverage --coverage-output-format cobertura - -# Multiple formats -dotnet run --configuration Release --coverage --coverage-output-format cobertura --coverage-output-format xml -``` - -#### 4. Update CI/CD Pipelines - -If you have CI/CD pipelines that reference Coverlet, update them to use the new commands: - -**GitHub Actions Example:** -```yaml -# Old (NUnit with Coverlet) -- name: Run tests with coverage - run: dotnet test --collect:"XPlat Code Coverage" - -# New (TUnit with Microsoft Coverage) -- name: Run tests with coverage - run: dotnet run --project ./tests/MyProject.Tests --configuration Release --coverage -``` - -**Azure Pipelines Example:** -```yaml -# Old (NUnit with Coverlet) -- task: DotNetCoreCLI@2 - inputs: - command: 'test' - arguments: '--collect:"XPlat Code Coverage"' - -# New (TUnit with Microsoft Coverage) -- task: DotNetCoreCLI@2 - inputs: - command: 'run' - arguments: '--configuration Release --coverage --coverage-output $(Agent.TempDirectory)/coverage/' -``` - -### Coverage Output Formats - -The Microsoft coverage tool supports multiple output formats: - -```bash -# Cobertura (default, widely supported) -dotnet run --configuration Release --coverage --coverage-output-format cobertura - -# XML (Visual Studio format) -dotnet run --configuration Release --coverage --coverage-output-format xml - -# Cobertura + XML -dotnet run --configuration Release --coverage \ - --coverage-output-format cobertura \ - --coverage-output-format xml -``` - -### Viewing Coverage Results - -Coverage files are generated in your test output directory: - -``` -TestResults/ - ├── coverage.cobertura.xml - └── / - └── coverage.xml -``` - -You can view these with: -- **Visual Studio** - Built-in coverage viewer -- **VS Code** - Extensions like "Coverage Gutters" -- **ReportGenerator** - Generate HTML reports: `reportgenerator -reports:coverage.cobertura.xml -targetdir:coveragereport` -- **CI Tools** - Most CI systems can parse Cobertura format natively - -### Advanced Coverage Configuration - -You can customize coverage behavior with a `testconfig.json` file: - -**testconfig.json:** -```json -{ - "codeCoverage": { - "Configuration": { - "CodeCoverage": { - "ModulePaths": { - "Include": [".*\\.dll$"], - "Exclude": [".*tests\\.dll$"] - } - } - } - } -} -``` - -Place the `testconfig.json` file in the same directory as your test project. It will be picked up automatically when running tests. - -**Alternatively, you can use an XML coverage settings file:** -```bash -dotnet run --configuration Release --coverage --coverage-settings coverage.config -``` - -### Troubleshooting - -**Coverage files not generated?** -- Ensure you're using the TUnit meta package, not just TUnit.Engine -- Verify you have a recent .NET SDK installed - -**Missing coverage for some assemblies?** -- Use a `testconfig.json` file to explicitly include/exclude modules -- See [Microsoft's documentation](https://github.com/microsoft/codecoverage/blob/main/docs/configuration.md) - -**Need help?** -- See [TUnit Code Coverage Documentation](../extensions/extensions.md#code-coverage) -- Check [Microsoft's Code Coverage Guide](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage) \ No newline at end of file +See the [Code Coverage guide](../extensions/code-coverage.md) for setup and configuration. \ No newline at end of file diff --git a/docs/docs/migration/xunit.md b/docs/docs/migration/xunit.md index f059e84a89..b11656626a 100644 --- a/docs/docs/migration/xunit.md +++ b/docs/docs/migration/xunit.md @@ -1168,171 +1168,6 @@ public class UserServiceTests(DatabaseFixture dbFixture) ## Code Coverage -### Important: Coverlet is Not Compatible with TUnit +TUnit includes built-in code coverage support. Do **not** use Coverlet — it is incompatible with TUnit's Microsoft.Testing.Platform. -If you're using **Coverlet** (`coverlet.collector` or `coverlet.msbuild`) for code coverage in your xUnit projects, you'll need to migrate to **Microsoft.Testing.Extensions.CodeCoverage**. - -**Why?** TUnit uses the modern `Microsoft.Testing.Platform` instead of VSTest, and Coverlet only works with the legacy VSTest platform. - -### Good News: Coverage is Built In! 🎉 - -When you install the **TUnit** meta package, it automatically includes `Microsoft.Testing.Extensions.CodeCoverage` for you. You don't need to install it separately! - -### Migration Steps - -#### 1. Remove Coverlet Packages - -Remove any Coverlet packages from your project file: - -**Remove these lines from your `.csproj`:** -```xml - - - -``` - -#### 2. Verify TUnit Meta Package - -Ensure you're using the **TUnit** meta package (not just TUnit.Core): - -**Your `.csproj` should have:** -```xml - -``` - -This automatically brings in: -- `Microsoft.Testing.Extensions.CodeCoverage` (coverage support) -- `Microsoft.Testing.Extensions.TrxReport` (test result reports) - -#### 3. Update Your Coverage Commands - -Replace your old Coverlet commands with the new Microsoft coverage syntax: - -**Old (Coverlet with xUnit):** -```bash -# With coverlet.collector -dotnet test --collect:"XPlat Code Coverage" - -# With coverlet.msbuild -dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura -``` - -**New (TUnit with Microsoft Coverage):** -```bash -# Run tests with coverage -dotnet run --configuration Release --coverage - -# Specify output location -dotnet run --configuration Release --coverage --coverage-output ./coverage/ - -# Specify coverage format (default is cobertura) -dotnet run --configuration Release --coverage --coverage-output-format cobertura - -# Multiple formats -dotnet run --configuration Release --coverage --coverage-output-format cobertura --coverage-output-format xml -``` - -#### 4. Update CI/CD Pipelines - -If you have CI/CD pipelines that reference Coverlet, update them to use the new commands: - -**GitHub Actions Example:** -```yaml -# Old (xUnit with Coverlet) -- name: Run tests with coverage - run: dotnet test --collect:"XPlat Code Coverage" - -# New (TUnit with Microsoft Coverage) -- name: Run tests with coverage - run: dotnet run --project ./tests/MyProject.Tests --configuration Release --coverage -``` - -**Azure Pipelines Example:** -```yaml -# Old (xUnit with Coverlet) -- task: DotNetCoreCLI@2 - inputs: - command: 'test' - arguments: '--collect:"XPlat Code Coverage"' - -# New (TUnit with Microsoft Coverage) -- task: DotNetCoreCLI@2 - inputs: - command: 'run' - arguments: '--configuration Release --coverage --coverage-output $(Agent.TempDirectory)/coverage/' -``` - -### Coverage Output Formats - -The Microsoft coverage tool supports multiple output formats: - -```bash -# Cobertura (default, widely supported) -dotnet run --configuration Release --coverage --coverage-output-format cobertura - -# XML (Visual Studio format) -dotnet run --configuration Release --coverage --coverage-output-format xml - -# Cobertura + XML -dotnet run --configuration Release --coverage \ - --coverage-output-format cobertura \ - --coverage-output-format xml -``` - -### Viewing Coverage Results - -Coverage files are generated in your test output directory: - -``` -TestResults/ - ├── coverage.cobertura.xml - └── / - └── coverage.xml -``` - -You can view these with: -- **Visual Studio** - Built-in coverage viewer -- **VS Code** - Extensions like "Coverage Gutters" -- **ReportGenerator** - Generate HTML reports: `reportgenerator -reports:coverage.cobertura.xml -targetdir:coveragereport` -- **CI Tools** - Most CI systems can parse Cobertura format natively - -### Advanced Coverage Configuration - -You can customize coverage behavior with a `testconfig.json` file: - -**testconfig.json:** -```json -{ - "codeCoverage": { - "Configuration": { - "CodeCoverage": { - "ModulePaths": { - "Include": [".*\\.dll$"], - "Exclude": [".*tests\\.dll$"] - } - } - } - } -} -``` - -Place the `testconfig.json` file in the same directory as your test project. It will be picked up automatically when running tests. - -**Alternatively, you can use an XML coverage settings file:** -```bash -dotnet run --configuration Release --coverage --coverage-settings coverage.config -``` - -### Troubleshooting - -**Coverage files not generated?** -- Ensure you're using the TUnit meta package, not just TUnit.Engine -- Verify you have a recent .NET SDK installed - -**Missing coverage for some assemblies?** -- Use a `testconfig.json` file to explicitly include/exclude modules -- See [Microsoft's documentation](https://github.com/microsoft/codecoverage/blob/main/docs/configuration.md) - -**Need help?** -- See [TUnit Code Coverage Documentation](../extensions/extensions.md#code-coverage) -- Check [Microsoft's Code Coverage Guide](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage) +See the [Code Coverage guide](../extensions/code-coverage.md) for setup and configuration. diff --git a/docs/docs/test-authoring/class-data-source.md b/docs/docs/test-authoring/class-data-source.md index 4b157bfb1d..7b706e7d03 100644 --- a/docs/docs/test-authoring/class-data-source.md +++ b/docs/docs/test-authoring/class-data-source.md @@ -4,29 +4,11 @@ The `ClassDataSource` attribute is used to instantiate and inject in new classes The attribute takes a generic type argument, which is the type of data you want to inject into your test. -It also takes an optional `Shared` argument, controlling whether you want to share the instance among other tests. -This could be useful for times where it's very intensive to spin up lots of objects, and you instead want to share that same instance across many tests. +It also takes an optional `Shared` argument, controlling whether you want to share the instance among other tests. This is useful when it is expensive to create an object and you want to reuse the same instance across many tests. -Ideally don't manipulate the state of this object within your tests if your object is shared. Because of concurrency, it's impossible to know which test will run in which order, and so your tests could become flaky and undeterministic. +Avoid mutating the state of shared objects within tests. Because tests run concurrently, the execution order is unpredictable, and shared mutable state leads to flaky tests. -Options are: - -### Shared = SharedType.None (Default) -The instance is not shared ever. A new one will be created for you. This is the default if `Shared` is not specified. - -### Shared = SharedType.PerClass -The instance is shared for every test in the same class as itself, that also has this setting. - -### Shared = SharedType.PerAssembly -The instance is shared for every test in the same assembly as itself, that also has this setting. - -### Shared = SharedType.PerTestSession -The instance is shared for every test in the current test session, meaning it'll always be the same instance. - -### Shared = SharedType.Keyed -When using this, you must also populate the `Key` argument on the attribute. - -The instance is shared for every test that also has this setting, and also uses the same key. +The `SharedType` parameter controls how instances are shared across tests. See [Property Injection -- Sharing Strategies](../test-lifecycle/property-injection.md#sharing-strategies) for full details on the available options (`None`, `PerClass`, `PerAssembly`, `PerTestSession`, `Keyed`). ## Initialization and TearDown If you need to do some initialization or teardown for when this object is created/disposed, simply implement the `IAsyncInitializer` and/or `IAsyncDisposable` interfaces diff --git a/docs/docs/test-lifecycle/cleanup.md b/docs/docs/test-lifecycle/cleanup.md index 6c66d55408..b07d96190d 100644 --- a/docs/docs/test-lifecycle/cleanup.md +++ b/docs/docs/test-lifecycle/cleanup.md @@ -30,7 +30,11 @@ public async Task AsyncCleanup() // ✅ Valid - asynchronous hook ### Hook Parameters -Hooks can optionally accept parameters for accessing context information and cancellation tokens: +:::info +`[After]` hooks accept the same parameters as `[Before]` hooks. See [Setup Hooks -- Hook Parameters](setup.md#hook-parameters) for the full reference. +::: + +A common pattern in cleanup hooks is checking the test result to perform conditional cleanup: ```csharp [After(Test)] @@ -42,45 +46,8 @@ public async Task Cleanup(TestContext context, CancellationToken cancellationTok await CaptureScreenshot(cancellationToken); } } - -[After(Class)] -public static async Task ClassCleanup(ClassHookContext context, CancellationToken cancellationToken) -{ - // Use cancellation token for timeout-aware cleanup operations - await DisposeResources(cancellationToken); -} - -[After(Test)] -public async Task CleanupWithToken(CancellationToken cancellationToken) -{ - // Can use CancellationToken without context - await FlushBuffers(cancellationToken); -} - -[After(Test)] -public async Task CleanupWithContext(TestContext context) -{ - // Can use context without CancellationToken - Console.WriteLine($"Test {context.Metadata.TestName} completed"); -} ``` -**Valid Parameter Combinations:** -- No parameters: `public void Hook() { }` -- Context only: `public void Hook(TestContext context) { }` -- CancellationToken only: `public async Task Hook(CancellationToken ct) { }` -- Both: `public async Task Hook(TestContext context, CancellationToken ct) { }` - -**Context Types by Hook Level:** - -| Hook Level | Context Type | Example | -|------------|-------------|---------| -| `[After(Test)]` | `TestContext` | Access test results, output writer | -| `[After(Class)]` | `ClassHookContext` | Access class information | -| `[After(Assembly)]` | `AssemblyHookContext` | Access assembly information | -| `[After(TestSession)]` | `TestSessionContext` | Access test session information | -| `[After(TestDiscovery)]` | `TestDiscoveryContext` | Access discovery results | - ## [After(HookType)] ### [After(Test)] @@ -116,12 +83,6 @@ Will be executed after the last test of every class that will run in the test se ### [AfterEvery(Assembly)] Will be executed after the last test of every assembly that will run in the test session. -### [AfterEvery(TestSession)] -The same as [After(TestSession)] - -### [AfterEvery(TestDiscovery)] -The same as [After(TestDiscovery)] - ```csharp using TUnit.Core; diff --git a/docs/docs/test-lifecycle/setup.md b/docs/docs/test-lifecycle/setup.md index 125bb745a5..a66033d2c7 100644 --- a/docs/docs/test-lifecycle/setup.md +++ b/docs/docs/test-lifecycle/setup.md @@ -115,12 +115,6 @@ Will be executed before the first test of every class that will run in the test ### [BeforeEvery(Assembly)] Will be executed before the first test of every assembly that will run in the test session. -### [BeforeEvery(TestSession)] -The same as [Before(TestSession)] - -### [BeforeEvery(TestDiscovery)] -The same as [Before(TestDiscovery)] - ```csharp using TUnit.Core; From 93ec7e5ed85795ff939e3cf3126ef483e0fe3d55 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:29:15 +0000 Subject: [PATCH 4/9] docs: restructure directories and flatten sidebar navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorganize 7 old directories into 3 new ones: - test-lifecycle/ + test-authoring/ → writing-tests/ - advanced/ + customization-extensibility/ + extensions/ + experimental/ → extending/ - parallelism/ → execution/ (merged in) - advanced/performance-best-practices.md → guides/performance.md Rewrite sidebars.ts: - Remove emoji from all category labels - Promote Assertions to top-level (was buried 3 levels deep) - Flatten max nesting from 5 levels to 3 - Dissolve "Platform-Specific Scenarios" catch-all - Merge Extensibility + Customization + Extensions into one section Update all internal cross-references across 14 files. Add missing frontmatter to 5 assertion docs. Delete 4 stale _category_.json files. --- docs/docs/assertions/awaiting.md | 4 + .../extensibility-chaining-and-converting.md | 4 + ...xtensibility-returning-items-from-await.md | 4 + docs/docs/assertions/member-assertions.md | 4 + docs/docs/assertions/regex-assertions.md | 4 + docs/docs/benchmarks/methodology.md | 2 +- docs/docs/comparison/_category_.json | 7 - docs/docs/examples/_category_.json | 7 - .../not-in-parallel.md | 0 .../parallel-groups.md | 0 .../parallel-limiter.md | 0 docs/docs/execution/test-filters.md | 2 +- docs/docs/experimental/_category_.json | 8 - .../argument-formatters.md | 0 .../built-in-extensions.md} | 0 .../code-coverage.md | 0 .../data-source-generators.md | 0 .../display-names.md | 2 +- .../dynamic-tests.md | 0 .../exception-handling.md | 0 .../extension-points.md | 2 +- .../libraries.md | 0 .../logging.md | 0 .../{advanced => extending}/test-variants.md | 7 +- docs/docs/extensions/_category_.json | 7 - docs/docs/getting-started/installation.md | 4 +- .../getting-started/running-your-tests.md | 10 +- docs/docs/guides/best-practices.md | 4 +- docs/docs/guides/cookbook.md | 2 +- .../performance.md} | 0 docs/docs/migration/mstest.md | 4 +- docs/docs/migration/nunit.md | 4 +- docs/docs/migration/xunit.md | 4 +- docs/docs/reference/command-line-flags.md | 2 +- docs/docs/troubleshooting.md | 2 +- .../aot.md} | 0 .../arguments.md | 0 .../artifacts.md | 0 .../class-data-source.md | 2 +- .../combined-data-source.md | 0 .../culture.md | 0 .../dependency-injection.md | 2 +- .../event-subscribing.md | 0 .../explicit.md | 0 .../generic-attributes.md | 0 .../hooks-cleanup.md} | 0 .../setup.md => writing-tests/hooks-setup.md} | 0 .../lifecycle.md} | 0 .../matrix-tests.md | 0 .../method-data-source.md | 2 +- .../mocking/advanced.md | 0 .../mocking/argument-matchers.md | 0 .../mocking/http.md | 0 .../mocking/index.md | 0 .../mocking/logging.md | 0 .../mocking/setup.md | 0 .../mocking/verification.md | 0 .../nested-data-sources.md | 0 .../ordering.md} | 0 .../property-injection.md | 0 .../{test-authoring => writing-tests}/skip.md | 0 .../test-context.md | 0 .../test-data-row.md | 2 +- .../things-to-know.md | 0 docs/sidebars.ts | 349 +++++++----------- 65 files changed, 184 insertions(+), 273 deletions(-) delete mode 100644 docs/docs/comparison/_category_.json delete mode 100644 docs/docs/examples/_category_.json rename docs/docs/{parallelism => execution}/not-in-parallel.md (100%) rename docs/docs/{parallelism => execution}/parallel-groups.md (100%) rename docs/docs/{parallelism => execution}/parallel-limiter.md (100%) delete mode 100644 docs/docs/experimental/_category_.json rename docs/docs/{customization-extensibility => extending}/argument-formatters.md (100%) rename docs/docs/{extensions/extensions.md => extending/built-in-extensions.md} (100%) rename docs/docs/{extensions => extending}/code-coverage.md (100%) rename docs/docs/{customization-extensibility => extending}/data-source-generators.md (100%) rename docs/docs/{customization-extensibility => extending}/display-names.md (93%) rename docs/docs/{experimental => extending}/dynamic-tests.md (100%) rename docs/docs/{advanced => extending}/exception-handling.md (100%) rename docs/docs/{advanced => extending}/extension-points.md (99%) rename docs/docs/{customization-extensibility => extending}/libraries.md (100%) rename docs/docs/{customization-extensibility => extending}/logging.md (100%) rename docs/docs/{advanced => extending}/test-variants.md (97%) delete mode 100644 docs/docs/extensions/_category_.json rename docs/docs/{advanced/performance-best-practices.md => guides/performance.md} (100%) rename docs/docs/{test-authoring/aot-compatibility.md => writing-tests/aot.md} (100%) rename docs/docs/{test-authoring => writing-tests}/arguments.md (100%) rename docs/docs/{test-lifecycle => writing-tests}/artifacts.md (100%) rename docs/docs/{test-authoring => writing-tests}/class-data-source.md (92%) rename docs/docs/{test-authoring => writing-tests}/combined-data-source.md (100%) rename docs/docs/{test-authoring => writing-tests}/culture.md (100%) rename docs/docs/{test-lifecycle => writing-tests}/dependency-injection.md (95%) rename docs/docs/{test-lifecycle => writing-tests}/event-subscribing.md (100%) rename docs/docs/{test-authoring => writing-tests}/explicit.md (100%) rename docs/docs/{test-authoring => writing-tests}/generic-attributes.md (100%) rename docs/docs/{test-lifecycle/cleanup.md => writing-tests/hooks-cleanup.md} (100%) rename docs/docs/{test-lifecycle/setup.md => writing-tests/hooks-setup.md} (100%) rename docs/docs/{test-lifecycle/lifecycle-overview.md => writing-tests/lifecycle.md} (100%) rename docs/docs/{test-authoring => writing-tests}/matrix-tests.md (100%) rename docs/docs/{test-authoring => writing-tests}/method-data-source.md (98%) rename docs/docs/{test-authoring => writing-tests}/mocking/advanced.md (100%) rename docs/docs/{test-authoring => writing-tests}/mocking/argument-matchers.md (100%) rename docs/docs/{test-authoring => writing-tests}/mocking/http.md (100%) rename docs/docs/{test-authoring => writing-tests}/mocking/index.md (100%) rename docs/docs/{test-authoring => writing-tests}/mocking/logging.md (100%) rename docs/docs/{test-authoring => writing-tests}/mocking/setup.md (100%) rename docs/docs/{test-authoring => writing-tests}/mocking/verification.md (100%) rename docs/docs/{test-authoring => writing-tests}/nested-data-sources.md (100%) rename docs/docs/{test-authoring/depends-on.md => writing-tests/ordering.md} (100%) rename docs/docs/{test-lifecycle => writing-tests}/property-injection.md (100%) rename docs/docs/{test-authoring => writing-tests}/skip.md (100%) rename docs/docs/{test-lifecycle => writing-tests}/test-context.md (100%) rename docs/docs/{test-authoring => writing-tests}/test-data-row.md (98%) rename docs/docs/{test-authoring => writing-tests}/things-to-know.md (100%) diff --git a/docs/docs/assertions/awaiting.md b/docs/docs/assertions/awaiting.md index 540a19102c..11cb222090 100644 --- a/docs/docs/assertions/awaiting.md +++ b/docs/docs/assertions/awaiting.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 1 +--- + # Awaiting In TUnit you `await` your assertions, and this serves two purposes: diff --git a/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md b/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md index 59e748ad0f..175fedeee6 100644 --- a/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md +++ b/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 3 +--- + # Chaining and Converting TUnit allows you to chain assertions that change the type being asserted, enabling fluent and expressive test code. diff --git a/docs/docs/assertions/extensibility/extensibility-returning-items-from-await.md b/docs/docs/assertions/extensibility/extensibility-returning-items-from-await.md index 1a146cc999..e975899052 100644 --- a/docs/docs/assertions/extensibility/extensibility-returning-items-from-await.md +++ b/docs/docs/assertions/extensibility/extensibility-returning-items-from-await.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 4 +--- + # Returning Data via `await` Sometimes, you may want your assertion to return a value, such as an item found in a collection, so you can use it in further assertions or logic. diff --git a/docs/docs/assertions/member-assertions.md b/docs/docs/assertions/member-assertions.md index c0d8d7cec4..a1b8b34d4c 100644 --- a/docs/docs/assertions/member-assertions.md +++ b/docs/docs/assertions/member-assertions.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 12 +--- + # Member Assertions The `.Member()` method allows you to assert on object properties while maintaining the parent object's context for chaining. This is useful when you need to validate multiple properties of the same object. diff --git a/docs/docs/assertions/regex-assertions.md b/docs/docs/assertions/regex-assertions.md index 048aefc022..59f7ed36cf 100644 --- a/docs/docs/assertions/regex-assertions.md +++ b/docs/docs/assertions/regex-assertions.md @@ -1,3 +1,7 @@ +--- +sidebar_position: 13 +--- + # Regex Assertions The `.Matches()` method allows you to validate strings against regular expressions and assert on capture groups, match positions, and match lengths. This is useful when you need to validate structured text like emails, phone numbers, dates, or extract specific parts of a string. diff --git a/docs/docs/benchmarks/methodology.md b/docs/docs/benchmarks/methodology.md index d3649e163d..b9fa06d5b1 100644 --- a/docs/docs/benchmarks/methodology.md +++ b/docs/docs/benchmarks/methodology.md @@ -285,6 +285,6 @@ Found an issue with the benchmarks? [Open an issue](https://github.com/thomhurst - [BenchmarkDotNet Documentation](https://benchmarkdotnet.org/articles/overview.html) - [.NET Performance Best Practices](https://learn.microsoft.com/en-us/dotnet/framework/performance/) -- [TUnit Performance Best Practices](/docs/advanced/performance-best-practices) +- [TUnit Performance Best Practices](/docs/guides/performance) *Last updated: {new Date().toISOString().split('T')[0]}* diff --git a/docs/docs/comparison/_category_.json b/docs/docs/comparison/_category_.json deleted file mode 100644 index e6c3400976..0000000000 --- a/docs/docs/comparison/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Test Framework Comparisons", - "position": 5, - "link": { - "type": "generated-index" - } -} diff --git a/docs/docs/examples/_category_.json b/docs/docs/examples/_category_.json deleted file mode 100644 index 854948bfae..0000000000 --- a/docs/docs/examples/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Examples", - "position": 6, - "link": { - "type": "generated-index" - } -} diff --git a/docs/docs/parallelism/not-in-parallel.md b/docs/docs/execution/not-in-parallel.md similarity index 100% rename from docs/docs/parallelism/not-in-parallel.md rename to docs/docs/execution/not-in-parallel.md diff --git a/docs/docs/parallelism/parallel-groups.md b/docs/docs/execution/parallel-groups.md similarity index 100% rename from docs/docs/parallelism/parallel-groups.md rename to docs/docs/execution/parallel-groups.md diff --git a/docs/docs/parallelism/parallel-limiter.md b/docs/docs/execution/parallel-limiter.md similarity index 100% rename from docs/docs/parallelism/parallel-limiter.md rename to docs/docs/execution/parallel-limiter.md diff --git a/docs/docs/execution/test-filters.md b/docs/docs/execution/test-filters.md index 4b527923c8..2c19480dab 100644 --- a/docs/docs/execution/test-filters.md +++ b/docs/docs/execution/test-filters.md @@ -35,7 +35,7 @@ or `dotnet run --treenode-filter /*/*/*/AcceptCookiesTest` - To run all tests with the name `AcceptCookiesTest` -TUnit also supports filtering by your own [properties](../test-lifecycle/properties.md). So you could do: +TUnit also supports filtering by your own [properties](../writing-tests/test-context.md#custom-properties). So you could do: `dotnet run --treenode-filter /*/*/*/*[MyFilterName=*SomeValue*]` diff --git a/docs/docs/experimental/_category_.json b/docs/docs/experimental/_category_.json deleted file mode 100644 index 4b1789b932..0000000000 --- a/docs/docs/experimental/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Experimental", - "position": 7, - "link": { - "type": "generated-index", - "description": "Documentation for Experimental Features." - } -} diff --git a/docs/docs/customization-extensibility/argument-formatters.md b/docs/docs/extending/argument-formatters.md similarity index 100% rename from docs/docs/customization-extensibility/argument-formatters.md rename to docs/docs/extending/argument-formatters.md diff --git a/docs/docs/extensions/extensions.md b/docs/docs/extending/built-in-extensions.md similarity index 100% rename from docs/docs/extensions/extensions.md rename to docs/docs/extending/built-in-extensions.md diff --git a/docs/docs/extensions/code-coverage.md b/docs/docs/extending/code-coverage.md similarity index 100% rename from docs/docs/extensions/code-coverage.md rename to docs/docs/extending/code-coverage.md diff --git a/docs/docs/customization-extensibility/data-source-generators.md b/docs/docs/extending/data-source-generators.md similarity index 100% rename from docs/docs/customization-extensibility/data-source-generators.md rename to docs/docs/extending/data-source-generators.md diff --git a/docs/docs/customization-extensibility/display-names.md b/docs/docs/extending/display-names.md similarity index 93% rename from docs/docs/customization-extensibility/display-names.md rename to docs/docs/extending/display-names.md index e4bc5f7c93..6e66a15f5b 100644 --- a/docs/docs/customization-extensibility/display-names.md +++ b/docs/docs/extending/display-names.md @@ -28,7 +28,7 @@ The above would generate two test cases with their respective display name as: - "Test with: foo 1 True" - "Test with: bar 2 False" -If you have custom classes, you can combine this with [Argument Formatters](customization-extensibility/argument-formatters.md) to specify how to show them. +If you have custom classes, you can combine this with [Argument Formatters](argument-formatters.md) to specify how to show them. :::info If you want to include a literal `$` in your display name, escape it as `$$`. diff --git a/docs/docs/experimental/dynamic-tests.md b/docs/docs/extending/dynamic-tests.md similarity index 100% rename from docs/docs/experimental/dynamic-tests.md rename to docs/docs/extending/dynamic-tests.md diff --git a/docs/docs/advanced/exception-handling.md b/docs/docs/extending/exception-handling.md similarity index 100% rename from docs/docs/advanced/exception-handling.md rename to docs/docs/extending/exception-handling.md diff --git a/docs/docs/advanced/extension-points.md b/docs/docs/extending/extension-points.md similarity index 99% rename from docs/docs/advanced/extension-points.md rename to docs/docs/extending/extension-points.md index 39980c6aac..188dd09b73 100644 --- a/docs/docs/advanced/extension-points.md +++ b/docs/docs/extending/extension-points.md @@ -509,7 +509,7 @@ public class MyTests } ``` -See [Property Injection - Discovery Phase Initialization](../test-lifecycle/property-injection.md#discovery-phase-initialization) for detailed guidance and best practices. +See [Property Injection - Discovery Phase Initialization](../writing-tests/property-injection.md#discovery-phase-initialization) for detailed guidance and best practices. ## Best Practices diff --git a/docs/docs/customization-extensibility/libraries.md b/docs/docs/extending/libraries.md similarity index 100% rename from docs/docs/customization-extensibility/libraries.md rename to docs/docs/extending/libraries.md diff --git a/docs/docs/customization-extensibility/logging.md b/docs/docs/extending/logging.md similarity index 100% rename from docs/docs/customization-extensibility/logging.md rename to docs/docs/extending/logging.md diff --git a/docs/docs/advanced/test-variants.md b/docs/docs/extending/test-variants.md similarity index 97% rename from docs/docs/advanced/test-variants.md rename to docs/docs/extending/test-variants.md index 0a34081955..5c5bde8912 100644 --- a/docs/docs/advanced/test-variants.md +++ b/docs/docs/extending/test-variants.md @@ -483,8 +483,7 @@ for (int i = 0; i < 10000; i++) // Creates 10,000 tests! ## See Also -- [Test Context](../test-lifecycle/test-context.md) - Understanding TestContext and StateBag -- [Dynamic Tests](../experimental/dynamic-tests.md) - Pre-execution test generation +- [Test Context](../writing-tests/test-context.md) - Understanding TestContext and StateBag +- [Dynamic Tests](dynamic-tests.md) - Pre-execution test generation - [Retrying](../execution/retrying.md) - Built-in retry mechanism comparison -- [Properties](../test-lifecycle/properties.md) - Test metadata and custom properties -- [Event Subscribing](../test-lifecycle/event-subscribing.md) - Test lifecycle event receivers +- [Event Subscribing](../writing-tests/event-subscribing.md) - Test lifecycle event receivers diff --git a/docs/docs/extensions/_category_.json b/docs/docs/extensions/_category_.json deleted file mode 100644 index 09441907fa..0000000000 --- a/docs/docs/extensions/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Extensions", - "position": 5, - "link": { - "type": "generated-index" - } -} diff --git a/docs/docs/getting-started/installation.md b/docs/docs/getting-started/installation.md index 31ccb0421d..e41ed3665b 100644 --- a/docs/docs/getting-started/installation.md +++ b/docs/docs/getting-started/installation.md @@ -81,8 +81,8 @@ dotnet run --configuration Release --coverage --report-trx **Important:** Do **not** install `coverlet.collector` or `coverlet.msbuild`. These packages are incompatible with TUnit because they require the VSTest platform, while TUnit uses the modern Microsoft.Testing.Platform. For more details, see: -- [Code Coverage Documentation](../extensions/extensions.md#code-coverage) -- [Extensions Overview](../extensions/extensions.md) +- [Code Coverage Documentation](../extending/built-in-extensions.md#code-coverage) +- [Extensions Overview](../extending/built-in-extensions.md) That's it. We're ready to write our first test. diff --git a/docs/docs/getting-started/running-your-tests.md b/docs/docs/getting-started/running-your-tests.md index 9b79af6f98..ce9a202225 100644 --- a/docs/docs/getting-started/running-your-tests.md +++ b/docs/docs/getting-started/running-your-tests.md @@ -4,7 +4,7 @@ As TUnit is built on-top of the newer Microsoft.Testing.Platform, and combined w :::info -Please note that for the coverage and trx report, you need to install [additional extensions](../extensions/extensions.md) +Please note that for the coverage and trx report, you need to install [additional extensions](../extending/built-in-extensions.md) ::: @@ -98,16 +98,16 @@ To continue your journey with TUnit, explore these topics: **Core Testing Concepts:** - **[Assertions](../assertions/getting-started.md)** - Learn TUnit's fluent assertion syntax -- **[Test Lifecycle](../test-lifecycle/setup.md)** - Set up and tear down test state with hooks -- **[Data-Driven Testing](../test-authoring/arguments.md)** - Run tests with multiple input values +- **[Test Lifecycle](../writing-tests/hooks-setup.md)** - Set up and tear down test state with hooks +- **[Data-Driven Testing](../writing-tests/arguments.md)** - Run tests with multiple input values **Common Tasks:** -- **[Mocking](../test-authoring/mocking/index.md)** - Use mocks and fakes in your tests +- **[Mocking](../writing-tests/mocking/index.md)** - Use mocks and fakes in your tests - **[Best Practices](../guides/best-practices.md)** - Write maintainable, reliable tests - **[Cookbook](../guides/cookbook.md)** - Common testing patterns and recipes **Advanced Features:** -- **[Parallelism](../parallelism/not-in-parallel.md)** - Control how tests run in parallel +- **[Parallelism](../execution/not-in-parallel.md)** - Control how tests run in parallel - **[CI/CD Integration](../execution/ci-cd-reporting.md)** - Integrate TUnit into your pipeline Need help? Check the [Troubleshooting & FAQ](../troubleshooting.md) guide. diff --git a/docs/docs/guides/best-practices.md b/docs/docs/guides/best-practices.md index 56f998cef8..38ef3436da 100644 --- a/docs/docs/guides/best-practices.md +++ b/docs/docs/guides/best-practices.md @@ -658,7 +658,7 @@ TUnit is designed for performance at scale. Follow these guidelines to keep your ### Optimize Test Discovery - Use AOT mode for faster test discovery and lower memory usage -- Keep data sources lightweight (see [Performance Best Practices](../advanced/performance-best-practices.md)) +- Keep data sources lightweight (see [Performance Best Practices](performance.md)) - Limit matrix test combinations to avoid test explosion ### Optimize Test Execution @@ -692,7 +692,7 @@ public async Task GetUserData() } ``` -For detailed performance guidance, see [Performance Best Practices](../advanced/performance-best-practices.md). +For detailed performance guidance, see [Performance Best Practices](performance.md). ## Summary diff --git a/docs/docs/guides/cookbook.md b/docs/docs/guides/cookbook.md index 53e0bd1488..4dd452d635 100644 --- a/docs/docs/guides/cookbook.md +++ b/docs/docs/guides/cookbook.md @@ -328,7 +328,7 @@ public class OrderServiceTUnitMocksTests - **AOT-compatible** — source-generated mocks work with Native AOT, trimming, and single-file publishing - **Built-in capture** — every `Arg` automatically captures values for inspection via `.Values` and `.Latest` -See the full [TUnit.Mocks documentation](../test-authoring/mocking) for setup, verification, argument matchers, and more. +See the full [TUnit.Mocks documentation](../writing-tests/mocking) for setup, verification, argument matchers, and more. ::: ### Partial Mocks and Spy Pattern diff --git a/docs/docs/advanced/performance-best-practices.md b/docs/docs/guides/performance.md similarity index 100% rename from docs/docs/advanced/performance-best-practices.md rename to docs/docs/guides/performance.md diff --git a/docs/docs/migration/mstest.md b/docs/docs/migration/mstest.md index 6000f9038c..e01815300f 100644 --- a/docs/docs/migration/mstest.md +++ b/docs/docs/migration/mstest.md @@ -464,7 +464,7 @@ public async Task TestWithAttachment() } ``` -For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../test-lifecycle/artifacts.md). +For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../writing-tests/artifacts.md). ### Assert.Fail @@ -1124,4 +1124,4 @@ public class ContextTests 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](../extensions/code-coverage.md) for setup and configuration. \ No newline at end of file +See the [Code Coverage guide](../extending/code-coverage.md) for setup and configuration. \ No newline at end of file diff --git a/docs/docs/migration/nunit.md b/docs/docs/migration/nunit.md index 1dcedea5cd..cf1cfc7c43 100644 --- a/docs/docs/migration/nunit.md +++ b/docs/docs/migration/nunit.md @@ -394,7 +394,7 @@ public async Task TestWithAttachment() } ``` -For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../test-lifecycle/artifacts.md). +For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../writing-tests/artifacts.md). ### Combinatorial Testing @@ -840,4 +840,4 @@ public static class AssemblyHooks 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](../extensions/code-coverage.md) for setup and configuration. \ No newline at end of file +See the [Code Coverage guide](../extending/code-coverage.md) for setup and configuration. \ No newline at end of file diff --git a/docs/docs/migration/xunit.md b/docs/docs/migration/xunit.md index b11656626a..1d6eb11774 100644 --- a/docs/docs/migration/xunit.md +++ b/docs/docs/migration/xunit.md @@ -859,7 +859,7 @@ public class TestWithAttachments } ``` -For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../test-lifecycle/artifacts.md). +For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../writing-tests/artifacts.md). ### Traits and Categories @@ -1170,4 +1170,4 @@ public class UserServiceTests(DatabaseFixture dbFixture) 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](../extensions/code-coverage.md) for setup and configuration. +See the [Code Coverage guide](../extending/code-coverage.md) for setup and configuration. diff --git a/docs/docs/reference/command-line-flags.md b/docs/docs/reference/command-line-flags.md index 471c433eae..d62f84db8b 100644 --- a/docs/docs/reference/command-line-flags.md +++ b/docs/docs/reference/command-line-flags.md @@ -2,7 +2,7 @@ :::info -Please note that for the coverage and trx report, you need to install [additional extensions](../extensions/extensions.md) +Please note that for the coverage and trx report, you need to install [additional extensions](../extending/built-in-extensions.md) ::: diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index 4dc7b02c04..9dc6965665 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -274,7 +274,7 @@ public class Fixture : IAsyncDiscoveryInitializer // Changed from IAsyncInitial **Performance note:** Solution 1 is preferred because discovery happens frequently (IDE reloads, project switches, `--list-tests`), and you want to avoid expensive operations during discovery when possible. -For detailed examples, see [Property Injection - Discovery Phase Initialization](test-lifecycle/property-injection.md#discovery-phase-initialization). +For detailed examples, see [Property Injection - Discovery Phase Initialization](writing-tests/property-injection.md#discovery-phase-initialization). ## Test Execution Issues diff --git a/docs/docs/test-authoring/aot-compatibility.md b/docs/docs/writing-tests/aot.md similarity index 100% rename from docs/docs/test-authoring/aot-compatibility.md rename to docs/docs/writing-tests/aot.md diff --git a/docs/docs/test-authoring/arguments.md b/docs/docs/writing-tests/arguments.md similarity index 100% rename from docs/docs/test-authoring/arguments.md rename to docs/docs/writing-tests/arguments.md diff --git a/docs/docs/test-lifecycle/artifacts.md b/docs/docs/writing-tests/artifacts.md similarity index 100% rename from docs/docs/test-lifecycle/artifacts.md rename to docs/docs/writing-tests/artifacts.md diff --git a/docs/docs/test-authoring/class-data-source.md b/docs/docs/writing-tests/class-data-source.md similarity index 92% rename from docs/docs/test-authoring/class-data-source.md rename to docs/docs/writing-tests/class-data-source.md index 7b706e7d03..4a66907d0e 100644 --- a/docs/docs/test-authoring/class-data-source.md +++ b/docs/docs/writing-tests/class-data-source.md @@ -8,7 +8,7 @@ It also takes an optional `Shared` argument, controlling whether you want to sha Avoid mutating the state of shared objects within tests. Because tests run concurrently, the execution order is unpredictable, and shared mutable state leads to flaky tests. -The `SharedType` parameter controls how instances are shared across tests. See [Property Injection -- Sharing Strategies](../test-lifecycle/property-injection.md#sharing-strategies) for full details on the available options (`None`, `PerClass`, `PerAssembly`, `PerTestSession`, `Keyed`). +The `SharedType` parameter controls how instances are shared across tests. See [Property Injection -- Sharing Strategies](property-injection.md#sharing-strategies) for full details on the available options (`None`, `PerClass`, `PerAssembly`, `PerTestSession`, `Keyed`). ## Initialization and TearDown If you need to do some initialization or teardown for when this object is created/disposed, simply implement the `IAsyncInitializer` and/or `IAsyncDisposable` interfaces diff --git a/docs/docs/test-authoring/combined-data-source.md b/docs/docs/writing-tests/combined-data-source.md similarity index 100% rename from docs/docs/test-authoring/combined-data-source.md rename to docs/docs/writing-tests/combined-data-source.md diff --git a/docs/docs/test-authoring/culture.md b/docs/docs/writing-tests/culture.md similarity index 100% rename from docs/docs/test-authoring/culture.md rename to docs/docs/writing-tests/culture.md diff --git a/docs/docs/test-lifecycle/dependency-injection.md b/docs/docs/writing-tests/dependency-injection.md similarity index 95% rename from docs/docs/test-lifecycle/dependency-injection.md rename to docs/docs/writing-tests/dependency-injection.md index 4731ca3396..c0390b1fab 100644 --- a/docs/docs/test-lifecycle/dependency-injection.md +++ b/docs/docs/writing-tests/dependency-injection.md @@ -30,7 +30,7 @@ public class MyTestClass(SomeDependency dep) } ``` -The `ClassConstructorMetadata` parameter provides context about the test being constructed, including the test's data-source arguments and metadata. You can also implement [event-subscribing interfaces](test-lifecycle/event-subscribing.md) on the same class to get notified when a test finishes — useful for disposing objects after the test completes. +The `ClassConstructorMetadata` parameter provides context about the test being constructed, including the test's data-source arguments and metadata. You can also implement [event-subscribing interfaces](event-subscribing.md) on the same class to get notified when a test finishes — useful for disposing objects after the test completes. ## DependencyInjectionDataSourceAttribute diff --git a/docs/docs/test-lifecycle/event-subscribing.md b/docs/docs/writing-tests/event-subscribing.md similarity index 100% rename from docs/docs/test-lifecycle/event-subscribing.md rename to docs/docs/writing-tests/event-subscribing.md diff --git a/docs/docs/test-authoring/explicit.md b/docs/docs/writing-tests/explicit.md similarity index 100% rename from docs/docs/test-authoring/explicit.md rename to docs/docs/writing-tests/explicit.md diff --git a/docs/docs/test-authoring/generic-attributes.md b/docs/docs/writing-tests/generic-attributes.md similarity index 100% rename from docs/docs/test-authoring/generic-attributes.md rename to docs/docs/writing-tests/generic-attributes.md diff --git a/docs/docs/test-lifecycle/cleanup.md b/docs/docs/writing-tests/hooks-cleanup.md similarity index 100% rename from docs/docs/test-lifecycle/cleanup.md rename to docs/docs/writing-tests/hooks-cleanup.md diff --git a/docs/docs/test-lifecycle/setup.md b/docs/docs/writing-tests/hooks-setup.md similarity index 100% rename from docs/docs/test-lifecycle/setup.md rename to docs/docs/writing-tests/hooks-setup.md diff --git a/docs/docs/test-lifecycle/lifecycle-overview.md b/docs/docs/writing-tests/lifecycle.md similarity index 100% rename from docs/docs/test-lifecycle/lifecycle-overview.md rename to docs/docs/writing-tests/lifecycle.md diff --git a/docs/docs/test-authoring/matrix-tests.md b/docs/docs/writing-tests/matrix-tests.md similarity index 100% rename from docs/docs/test-authoring/matrix-tests.md rename to docs/docs/writing-tests/matrix-tests.md diff --git a/docs/docs/test-authoring/method-data-source.md b/docs/docs/writing-tests/method-data-source.md similarity index 98% rename from docs/docs/test-authoring/method-data-source.md rename to docs/docs/writing-tests/method-data-source.md index 92a6906e93..1342a9e699 100644 --- a/docs/docs/test-authoring/method-data-source.md +++ b/docs/docs/writing-tests/method-data-source.md @@ -271,4 +271,4 @@ public class MyTests 1. **Preferred:** Return predefined values that don't require initialization 2. **Alternative:** Use `IAsyncDiscoveryInitializer` if you need discovery-time initialization -See [Property Injection - Discovery Phase Initialization](../test-lifecycle/property-injection.md#discovery-phase-initialization) for detailed guidance and examples. +See [Property Injection - Discovery Phase Initialization](property-injection.md#discovery-phase-initialization) for detailed guidance and examples. diff --git a/docs/docs/test-authoring/mocking/advanced.md b/docs/docs/writing-tests/mocking/advanced.md similarity index 100% rename from docs/docs/test-authoring/mocking/advanced.md rename to docs/docs/writing-tests/mocking/advanced.md diff --git a/docs/docs/test-authoring/mocking/argument-matchers.md b/docs/docs/writing-tests/mocking/argument-matchers.md similarity index 100% rename from docs/docs/test-authoring/mocking/argument-matchers.md rename to docs/docs/writing-tests/mocking/argument-matchers.md diff --git a/docs/docs/test-authoring/mocking/http.md b/docs/docs/writing-tests/mocking/http.md similarity index 100% rename from docs/docs/test-authoring/mocking/http.md rename to docs/docs/writing-tests/mocking/http.md diff --git a/docs/docs/test-authoring/mocking/index.md b/docs/docs/writing-tests/mocking/index.md similarity index 100% rename from docs/docs/test-authoring/mocking/index.md rename to docs/docs/writing-tests/mocking/index.md diff --git a/docs/docs/test-authoring/mocking/logging.md b/docs/docs/writing-tests/mocking/logging.md similarity index 100% rename from docs/docs/test-authoring/mocking/logging.md rename to docs/docs/writing-tests/mocking/logging.md diff --git a/docs/docs/test-authoring/mocking/setup.md b/docs/docs/writing-tests/mocking/setup.md similarity index 100% rename from docs/docs/test-authoring/mocking/setup.md rename to docs/docs/writing-tests/mocking/setup.md diff --git a/docs/docs/test-authoring/mocking/verification.md b/docs/docs/writing-tests/mocking/verification.md similarity index 100% rename from docs/docs/test-authoring/mocking/verification.md rename to docs/docs/writing-tests/mocking/verification.md diff --git a/docs/docs/test-authoring/nested-data-sources.md b/docs/docs/writing-tests/nested-data-sources.md similarity index 100% rename from docs/docs/test-authoring/nested-data-sources.md rename to docs/docs/writing-tests/nested-data-sources.md diff --git a/docs/docs/test-authoring/depends-on.md b/docs/docs/writing-tests/ordering.md similarity index 100% rename from docs/docs/test-authoring/depends-on.md rename to docs/docs/writing-tests/ordering.md diff --git a/docs/docs/test-lifecycle/property-injection.md b/docs/docs/writing-tests/property-injection.md similarity index 100% rename from docs/docs/test-lifecycle/property-injection.md rename to docs/docs/writing-tests/property-injection.md diff --git a/docs/docs/test-authoring/skip.md b/docs/docs/writing-tests/skip.md similarity index 100% rename from docs/docs/test-authoring/skip.md rename to docs/docs/writing-tests/skip.md diff --git a/docs/docs/test-lifecycle/test-context.md b/docs/docs/writing-tests/test-context.md similarity index 100% rename from docs/docs/test-lifecycle/test-context.md rename to docs/docs/writing-tests/test-context.md diff --git a/docs/docs/test-authoring/test-data-row.md b/docs/docs/writing-tests/test-data-row.md similarity index 98% rename from docs/docs/test-authoring/test-data-row.md rename to docs/docs/writing-tests/test-data-row.md index 9ffb0124df..baa5e4c8c5 100644 --- a/docs/docs/test-authoring/test-data-row.md +++ b/docs/docs/writing-tests/test-data-row.md @@ -197,4 +197,4 @@ public class DatabaseTests - [Arguments Attribute](./arguments.md) - For compile-time constant data with inline metadata - [Method Data Sources](./method-data-source.md) - For dynamic test data generation - [Class Data Sources](./class-data-source.md) - For class-based test data -- [Display Names](../customization-extensibility/display-names.md) - For global display name formatting +- [Display Names](../extending/display-names.md) - For global display name formatting diff --git a/docs/docs/test-authoring/things-to-know.md b/docs/docs/writing-tests/things-to-know.md similarity index 100% rename from docs/docs/test-authoring/things-to-know.md rename to docs/docs/writing-tests/things-to-know.md diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 189e16a9d5..6546d1acd6 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -1,20 +1,10 @@ import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; -/** - * Creating a sidebar enables you to: - * - create an ordered group of docs - * - render a sidebar for each doc of that group - * - provide next/previous navigation - * - * The sidebars can be generated from the filesystem, or explicitly defined here. - * - * Create as many sidebars as you want. - */ const sidebars: SidebarsConfig = { docs: [ { type: 'category', - label: '🚀 Getting Started', + label: 'Getting Started', collapsed: false, items: [ 'intro', @@ -25,289 +15,220 @@ const sidebars: SidebarsConfig = { }, { type: 'category', - label: '🔄 Migration Guides', + label: 'Writing Tests', collapsed: true, items: [ - 'comparison/framework-differences', - 'migration/xunit', - 'migration/nunit', - 'migration/mstest', - 'migration/testcontext-interface-organization', - ], - }, - { - type: 'category', - label: '✍️ Test Authoring', - collapsed: true, - items: [ - 'test-authoring/things-to-know', + 'writing-tests/things-to-know', { type: 'category', - label: 'Core Concepts', - collapsed: false, + label: 'Lifecycle & Hooks', + collapsed: true, items: [ - 'test-lifecycle/lifecycle-overview', - 'test-lifecycle/setup', - 'test-lifecycle/cleanup', - 'test-lifecycle/test-context', - 'test-lifecycle/artifacts', - 'test-lifecycle/properties', - 'test-lifecycle/property-injection', - 'test-lifecycle/event-subscribing', - 'test-lifecycle/class-constructors', - 'test-lifecycle/dependency-injection', + 'writing-tests/lifecycle', + 'writing-tests/hooks-setup', + 'writing-tests/hooks-cleanup', + 'writing-tests/test-context', + 'writing-tests/artifacts', + 'writing-tests/property-injection', + 'writing-tests/dependency-injection', + 'writing-tests/event-subscribing', ], }, { type: 'category', - label: 'Assertions', - collapsed: false, + label: 'Test Data', + collapsed: true, items: [ - 'assertions/getting-started', - 'assertions/library', - { - type: 'category', - label: 'Core & Value Assertions', - collapsed: false, - items: [ - 'assertions/equality-and-comparison', - 'assertions/null-and-default', - 'assertions/boolean', - 'assertions/numeric', - 'assertions/string', - 'assertions/datetime', - 'assertions/types', - 'assertions/specialized-types', - ], - }, - { - type: 'category', - label: 'Collection Assertions', - collapsed: false, - items: [ - 'assertions/collections', - 'assertions/dictionaries', - ], - }, - { - type: 'category', - label: 'Async & Exception Assertions', - collapsed: false, - items: [ - 'assertions/awaiting', - 'assertions/tasks-and-async', - 'assertions/exceptions', - ], - }, - { - type: 'category', - label: 'Advanced & Composition', - collapsed: false, - items: [ - 'assertions/member-assertions', - 'assertions/and-conditions', - 'assertions/or-conditions', - 'assertions/scopes', - ], - }, - { - type: 'category', - label: 'Extending Assertions', - collapsed: false, - items: [ - 'assertions/extensibility/custom-assertions', - 'assertions/extensibility/source-generator-assertions', - 'assertions/extensibility/extensibility-chaining-and-converting', - 'assertions/extensibility/extensibility-returning-items-from-await', - ], - }, - ], - }, - { - type: 'category', - label: 'Data-Driven Testing', - collapsed: false, - items: [ - 'test-authoring/arguments', - 'test-authoring/method-data-source', - 'test-authoring/class-data-source', - 'test-authoring/test-data-row', - 'test-authoring/matrix-tests', - 'test-authoring/combined-data-source', - 'test-authoring/nested-data-sources', + 'writing-tests/arguments', + 'writing-tests/method-data-source', + 'writing-tests/class-data-source', + 'writing-tests/test-data-row', + 'writing-tests/matrix-tests', + 'writing-tests/combined-data-source', + 'writing-tests/nested-data-sources', + 'writing-tests/generic-attributes', ], }, { type: 'category', label: 'Controlling Execution', - collapsed: false, + collapsed: true, items: [ - 'test-authoring/skip', - 'test-authoring/explicit', - 'test-authoring/order', - 'test-authoring/depends-on', + 'writing-tests/skip', + 'writing-tests/explicit', + 'writing-tests/ordering', + 'writing-tests/culture', + 'writing-tests/aot', ], }, { type: 'category', label: 'Mocking', - collapsed: false, - items: [ - 'test-authoring/mocking/index', - 'test-authoring/mocking/setup', - 'test-authoring/mocking/verification', - 'test-authoring/mocking/argument-matchers', - 'test-authoring/mocking/advanced', - 'test-authoring/mocking/http', - 'test-authoring/mocking/logging', - ], - }, - { - type: 'category', - label: 'Advanced Techniques', - collapsed: false, + collapsed: true, items: [ - 'test-authoring/generic-attributes', + 'writing-tests/mocking/index', + 'writing-tests/mocking/setup', + 'writing-tests/mocking/verification', + 'writing-tests/mocking/argument-matchers', + 'writing-tests/mocking/advanced', + 'writing-tests/mocking/http', + 'writing-tests/mocking/logging', ], }, ], }, { type: 'category', - label: '⚙️ Running & Integrating', + label: 'Assertions', collapsed: true, items: [ - 'execution/test-filters', - 'execution/timeouts', - 'execution/retrying', - 'execution/repeating', + 'assertions/getting-started', + 'assertions/library', { type: 'category', - label: 'Parallelism', - collapsed: false, + label: 'Value Assertions', + collapsed: true, items: [ - 'parallelism/not-in-parallel', - 'parallelism/parallel-groups', - 'parallelism/parallel-limiter', + 'assertions/equality-and-comparison', + 'assertions/null-and-default', + 'assertions/boolean', + 'assertions/numeric', + 'assertions/string', + 'assertions/datetime', + 'assertions/types', + 'assertions/specialized-types', ], }, { type: 'category', - label: 'CI/CD & Reporting', - collapsed: false, + label: 'Collections', + collapsed: true, items: [ - 'execution/ci-cd-reporting', - 'examples/tunit-ci-pipeline', + 'assertions/collections', + 'assertions/dictionaries', ], }, { type: 'category', - label: 'Integrations & Tooling', - collapsed: false, + label: 'Async & Exceptions', + collapsed: true, items: [ - 'examples/aspnet', - 'examples/aspire', - 'examples/playwright', - 'examples/opentelemetry', - 'examples/fscheck', - 'examples/complex-test-infrastructure', + 'assertions/awaiting', + 'assertions/tasks-and-async', + 'assertions/exceptions', + ], + }, + 'assertions/combining-assertions', + 'assertions/member-assertions', + { + type: 'category', + label: 'Custom Assertions', + collapsed: true, + items: [ + 'assertions/extensibility/custom-assertions', + 'assertions/extensibility/source-generator-assertions', + 'assertions/extensibility/extensibility-chaining-and-converting', + 'assertions/extensibility/extensibility-returning-items-from-await', ], }, ], }, { type: 'category', - label: '💡 Guides & Best Practices', + label: 'Running Tests', collapsed: true, items: [ - 'guides/best-practices', - 'advanced/performance-best-practices', - 'guides/cookbook', - 'examples/intro', - 'examples/instrumenting-global-test-ids', + 'execution/test-filters', + 'execution/timeouts', + 'execution/retrying', + 'execution/repeating', { type: 'category', - label: 'Platform-Specific Scenarios', - collapsed: false, + label: 'Parallelism', + collapsed: true, items: [ - 'assertions/fsharp', - 'examples/fsharp-interactive', - 'test-authoring/aot-compatibility', - 'test-authoring/culture', - 'examples/filebased-csharp', + 'execution/not-in-parallel', + 'execution/parallel-groups', + 'execution/parallel-limiter', ], }, - 'troubleshooting', + 'execution/ci-cd-reporting', + 'execution/engine-modes', ], }, { type: 'category', - label: '🛠️ Extensibility', + label: 'Extending TUnit', collapsed: true, items: [ - { - type: 'category', - label: 'TUnit Internals', - collapsed: false, - items: [ - 'execution/executors', - 'execution/engine-modes', - 'advanced/exception-handling', - 'advanced/extension-points', - 'advanced/test-variants', - ], - }, - { - type: 'category', - label: 'Creating Extensions', - collapsed: false, - items: [ - 'customization-extensibility/data-source-generators', - 'customization-extensibility/argument-formatters', - 'customization-extensibility/display-names', - 'customization-extensibility/logging', - 'customization-extensibility/libraries', - ], - }, - { - type: 'category', - label: 'Built-in Extensions', - collapsed: false, - items: [ - 'extensions/extensions', - ], - }, - { - type: 'category', - label: 'Experimental Features', - collapsed: false, - items: [ - 'experimental/dynamic-tests', - ], - }, + 'extending/extension-points', + 'extending/data-source-generators', + 'extending/argument-formatters', + 'extending/display-names', + 'extending/logging', + 'extending/exception-handling', + 'extending/test-variants', + 'extending/dynamic-tests', + 'extending/built-in-extensions', + 'extending/code-coverage', + 'extending/libraries', ], }, { type: 'category', - label: '📚 Reference', + label: 'Migration Guides', + collapsed: true, + items: [ + 'comparison/framework-differences', + 'comparison/attributes', + 'migration/xunit', + 'migration/nunit', + 'migration/mstest', + 'migration/testcontext-interface-organization', + ], + }, + { + type: 'category', + label: 'Guides', + collapsed: true, + items: [ + 'guides/best-practices', + 'guides/performance', + 'guides/cookbook', + 'examples/aspnet', + 'examples/aspire', + 'examples/playwright', + 'examples/complex-test-infrastructure', + 'examples/fscheck', + 'examples/opentelemetry', + 'examples/filebased-csharp', + 'examples/fsharp-interactive', + 'examples/tunit-ci-pipeline', + 'examples/instrumenting-global-test-ids', + 'assertions/fsharp', + 'troubleshooting', + ], + }, + { + type: 'category', + label: 'Reference', collapsed: true, items: [ 'reference/command-line-flags', 'reference/environment-variables', 'reference/test-configuration', - 'comparison/attributes', ], }, { type: 'category', - label: 'ℹ️ About TUnit', + label: 'About TUnit', collapsed: true, items: [ 'guides/philosophy', { type: 'category', - label: 'Performance & Benchmarks', - collapsed: false, + label: 'Benchmarks', + collapsed: true, items: [ { type: 'autogenerated', From 965658b66e0d5d34cf07e6a55114ce84cca293a3 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:36:48 +0000 Subject: [PATCH 5/9] docs: normalize tone, trim padding, expand thin pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tone normalization: - Remove all first-person voice from framework-differences.md and things-to-know.md - Remove marketing copy from property-injection.md and philosophy.md - Remove emoji from headings across 13 files (benchmarks, installation, extensions, CI/CD) - Fix "tests tests" typo in framework-differences.md Padding trimmed (~1170 lines removed): - boolean.md: remove 7 filler sections (LINQ, string methods, type checking, etc.) - numeric.md: remove math library and arithmetic sections - datetime.md: remove quarter calculation, first/last day, timezone sections - equality-and-comparison.md: fix EqualTo alias, fix UtcNow double-call, remove redundant examples - collections.md: remove duplicate nested/find-and-assert sections - awaiting.md: strip grab-bag examples, keep core await explanation - property-injection.md: reduce 3 identical container examples to 1 - generic-attributes.md: remove fabricated patterns (Fibonacci, Builder, Factory, IJsonSerializable) - cookbook.md: replace duplicate TestWebServer with cross-reference - method-data-source.md: reduce 4 code blocks to 2 (Func pattern only) Pages expanded: - playwright.md: 80→400 words (real interaction test, browser config, parallel limits) - libraries.md: 50→250 words (csproj example, consumer usage) - test-configuration.md: 80→200 words (missing keys, nested syntax, typed values) - repeating.md: 130→250 words (Repeat vs Retry distinction, real test body) - timeouts.md: 140→250 words (cancellation behavior, real test body) - test-filters.md: 230→350 words (namespace, property, combined filters) - explicit.md: 130→200 words (concrete filter commands, category example) - parallel-groups.md: 85→250 words (clear semantics, step-by-step scenario) --- docs/docs/assertions/awaiting.md | 127 +-------- docs/docs/assertions/boolean.md | 265 +----------------- docs/docs/assertions/collections.md | 34 --- docs/docs/assertions/datetime.md | 56 ---- .../assertions/equality-and-comparison.md | 98 +------ docs/docs/assertions/numeric.md | 164 ----------- docs/docs/benchmarks/AsyncTests.md | 6 +- docs/docs/benchmarks/BuildTime.md | 4 +- docs/docs/benchmarks/DataDrivenTests.md | 6 +- docs/docs/benchmarks/MassiveParallelTests.md | 6 +- docs/docs/benchmarks/MatrixTests.md | 6 +- docs/docs/benchmarks/ScaleTests.md | 6 +- docs/docs/benchmarks/SetupTeardownTests.md | 6 +- docs/docs/benchmarks/index.md | 6 +- docs/docs/comparison/framework-differences.md | 19 +- docs/docs/examples/playwright.md | 104 ++++++- docs/docs/execution/ci-cd-reporting.md | 2 +- docs/docs/execution/parallel-groups.md | 73 +++-- docs/docs/execution/repeating.md | 21 +- docs/docs/execution/test-filters.md | 75 ++++- docs/docs/execution/timeouts.md | 36 ++- docs/docs/extending/built-in-extensions.md | 8 +- docs/docs/extending/libraries.md | 87 +++++- docs/docs/getting-started/installation.md | 12 +- docs/docs/guides/cookbook.md | 21 +- docs/docs/guides/philosophy.md | 8 - docs/docs/reference/test-configuration.md | 65 ++++- docs/docs/writing-tests/explicit.md | 59 +++- docs/docs/writing-tests/generic-attributes.md | 133 +-------- docs/docs/writing-tests/method-data-source.md | 76 +---- docs/docs/writing-tests/property-injection.md | 105 +------ docs/docs/writing-tests/things-to-know.md | 2 +- 32 files changed, 526 insertions(+), 1170 deletions(-) diff --git a/docs/docs/assertions/awaiting.md b/docs/docs/assertions/awaiting.md index 11cb222090..ffbb4c9841 100644 --- a/docs/docs/assertions/awaiting.md +++ b/docs/docs/assertions/awaiting.md @@ -179,77 +179,6 @@ public async Task CustomAssertionConditions() } ``` -### DateTime and TimeSpan Assertions - -```csharp -[Test] -public async Task DateTimeAssertions() -{ - var order = await CreateOrderAsync(); - - // Complex datetime assertions - await Assert.That(order.CreatedAt) - .IsGreaterThan(DateTime.UtcNow.AddMinutes(-1)) - .And.IsLessThan(DateTime.UtcNow.AddMinutes(1)); - - // TimeSpan assertions - var processingTime = order.CompletedAt - order.CreatedAt; - await Assert.That(processingTime) - .IsLessThan(TimeSpan.FromMinutes(5)) - .And.IsGreaterThan(TimeSpan.Zero); -} -``` - -### Floating Point Comparisons - -```csharp -[Test] -public async Task FloatingPointAssertions() -{ - var calculations = await PerformComplexCalculationsAsync(); - - // Use tolerance for floating point comparisons - await Assert.That(calculations.Pi) - .IsEqualTo(Math.PI).Within(0.0001); - - // Assert on collections of floating point numbers - await Assert.That(calculations.Results) - .All(r => Math.Abs(r) < 1000000); // No overflow - - // Check for approximate value in collection - var hasApproximate42 = calculations.Results.Any(r => Math.Abs(r - 42.0) < 0.1); - await Assert.That(hasApproximate42).IsTrue(); - - // Assert on sum with tolerance - var sum = calculations.Results.Sum(); - await Assert.That(sum).IsEqualTo(expectedSum).Within(0.01); -} -``` - -### String Pattern Matching - -```csharp -[Test] -public async Task StringPatternAssertions() -{ - var logs = await GetLogEntriesAsync(); - - // Complex string assertions - await Assert.That(logs) - .All(log => Regex.IsMatch(log, @"^\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]")) - .And.Any(log => log.Contains("ERROR")) - .And.DoesNotContain(log => log.Contains("SENSITIVE_DATA")); - - // Assert on formatted output - var report = await GenerateReportAsync(); - await Assert.That(report) - .StartsWith("Report Generated:") - .And.Contains("Total Items:") - .And.DoesNotContain("null") - .And.Length().IsBetween(1000, 5000); -} -``` - ### Combining Or and And Conditions ```csharp @@ -279,58 +208,4 @@ public async Task ComplexLogicalConditions() } ``` -### Performance Assertions - -```csharp -[Test] -public async Task PerformanceAssertions() -{ - var stopwatch = Stopwatch.StartNew(); - var results = new List(); - - // Measure multiple operations - for (int i = 0; i < 100; i++) - { - var start = stopwatch.ElapsedMilliseconds; - await PerformOperationAsync(); - results.Add(stopwatch.ElapsedMilliseconds - start); - } - - // Assert on performance metrics - await Assert.That(results.Average()) - .IsLessThan(100); // Average under 100ms - - await Assert.That(results.Max()) - .IsLessThan(500); // No operation over 500ms - - await Assert.That(results.Where(r => r > 200).Count()) - .IsLessThan(5); // Less than 5% over 200ms -} -``` - -### State Machine Assertions - -```csharp -[Test] -public async Task StateMachineAssertions() -{ - var workflow = new OrderWorkflow(); - - // Initial state - await Assert.That(workflow.State).IsEqualTo(OrderState.New); - - // State transition assertions - await workflow.StartProcessing(); - await Assert.That(workflow.State).IsEqualTo(OrderState.Processing); - await Assert.That(workflow.CanTransitionTo(OrderState.Completed)).IsTrue(); - await Assert.That(workflow.CanTransitionTo(OrderState.New)).IsFalse(); - - // Complex workflow validation - await workflow.Complete(); - await Assert.That(workflow.State).IsEqualTo(OrderState.Completed); - await Assert.That(workflow.CompletedAt).IsNotNull(); - await Assert.That(workflow.History).Contains(h => h.State == OrderState.Processing); -} -``` - -These examples demonstrate the power and flexibility of TUnit's assertion system, showing how you can build complex, readable assertions for various testing scenarios. +For more assertion examples, see the dedicated pages for [collections](collections.md), [strings](string.md), [numeric](numeric.md), and [datetime](datetime.md) assertions. diff --git a/docs/docs/assertions/boolean.md b/docs/docs/assertions/boolean.md index 3d15b4e4fb..6140d9e5e4 100644 --- a/docs/docs/assertions/boolean.md +++ b/docs/docs/assertions/boolean.md @@ -182,278 +182,25 @@ public async Task Feature_Toggles() } ``` -## Testing Conditional Logic +## Tip: Prefer Specific Assertions -### Logical AND +When testing the boolean result of a comparison, use the specific assertion instead for clearer failure messages: ```csharp [Test] -public async Task Logical_AND() -{ - var isAdult = age >= 18; - var hasLicense = CheckLicense(userId); - var canDrive = isAdult && hasLicense; - - await Assert.That(canDrive).IsTrue(); -} -``` - -### Logical OR - -```csharp -[Test] -public async Task Logical_OR() -{ - var isWeekend = dayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday; - var isHoliday = CheckIfHoliday(date); - var isDayOff = isWeekend || isHoliday; - - await Assert.That(isDayOff).IsTrue(); -} -``` - -### Logical NOT - -```csharp -[Test] -public async Task Logical_NOT() -{ - var isExpired = CheckExpiration(token); - var isValid = !isExpired; - - await Assert.That(isValid).IsTrue(); -} -``` - -## Complex Boolean Expressions - -```csharp -[Test] -public async Task Complex_Expression() -{ - var user = GetUser(); - var canAccess = user.IsActive && - !user.IsBanned && - (user.IsPremium || user.HasFreeTrial); - - await Assert.That(canAccess).IsTrue(); -} -``` - -You can also break this down for clarity: - -```csharp -[Test] -public async Task Complex_Expression_Broken_Down() -{ - var user = GetUser(); - - using (Assert.Multiple()) - { - await Assert.That(user.IsActive).IsTrue(); - await Assert.That(user.IsBanned).IsFalse(); - await Assert.That(user.IsPremium || user.HasFreeTrial).IsTrue(); - } -} -``` - -## Comparison with Other Values - -When testing boolean results of comparisons, you can often simplify: - -```csharp -[Test] -public async Task Comparison_Simplified() +public async Task Prefer_Specific_Assertions() { var count = GetCount(); - // Less clear: + // Less clear — failure message says "expected true but was false": await Assert.That(count > 0).IsTrue(); - // More clear and expressive: + // More clear — failure message shows the actual value: await Assert.That(count).IsGreaterThan(0); } ``` -Similarly for equality: - -```csharp -[Test] -public async Task Equality_Simplified() -{ - var name = GetName(); - - // Less clear: - await Assert.That(name == "Alice").IsTrue(); - - // More clear: - await Assert.That(name).IsEqualTo("Alice"); -} -``` - -Use boolean assertions for actual boolean values and flags, not for comparisons. - -## Testing LINQ Queries - -```csharp -[Test] -public async Task LINQ_Any() -{ - var numbers = new[] { 1, 2, 3, 4, 5 }; - - var hasEven = numbers.Any(n => n % 2 == 0); - await Assert.That(hasEven).IsTrue(); - - var hasNegative = numbers.Any(n => n < 0); - await Assert.That(hasNegative).IsFalse(); -} - -[Test] -public async Task LINQ_All() -{ - var numbers = new[] { 2, 4, 6, 8 }; - - var allEven = numbers.All(n => n % 2 == 0); - await Assert.That(allEven).IsTrue(); - - var allPositive = numbers.All(n => n > 0); - await Assert.That(allPositive).IsTrue(); -} -``` - -Note: TUnit provides specialized collection assertions for these patterns: - -```csharp -[Test] -public async Task Using_Collection_Assertions() -{ - var numbers = new[] { 2, 4, 6, 8 }; - - // Instead of .All(n => n % 2 == 0): - await Assert.That(numbers).All(n => n % 2 == 0); - - // Instead of .Any(n => n > 5): - await Assert.That(numbers).Any(n => n > 5); -} -``` - -## String Boolean Methods - -Many string methods return booleans: - -```csharp -[Test] -public async Task String_Boolean_Methods() -{ - var text = "Hello World"; - - await Assert.That(text.StartsWith("Hello")).IsTrue(); - await Assert.That(text.EndsWith("World")).IsTrue(); - await Assert.That(text.Contains("lo Wo")).IsTrue(); - await Assert.That(string.IsNullOrEmpty(text)).IsFalse(); -} -``` - -But TUnit has more expressive string assertions: - -```csharp -[Test] -public async Task Using_String_Assertions() -{ - var text = "Hello World"; - - // More expressive: - await Assert.That(text).StartsWith("Hello"); - await Assert.That(text).EndsWith("World"); - await Assert.That(text).Contains("lo Wo"); - await Assert.That(text).IsNotEmpty(); -} -``` - -## Type Checking Booleans - -```csharp -[Test] -public async Task Type_Checking() -{ - var obj = GetObject(); - - await Assert.That(obj is string).IsTrue(); - await Assert.That(obj is not null).IsTrue(); -} -``` - -Or use type assertions: - -```csharp -[Test] -public async Task Using_Type_Assertions() -{ - var obj = GetObject(); - - await Assert.That(obj).IsTypeOf(); - await Assert.That(obj).IsNotNull(); -} -``` - -## Common Patterns - -### Toggle Testing - -```csharp -[Test] -public async Task Toggle_State() -{ - var toggle = new Toggle(); - - await Assert.That(toggle.IsOn).IsFalse(); - - toggle.TurnOn(); - await Assert.That(toggle.IsOn).IsTrue(); - - toggle.TurnOff(); - await Assert.That(toggle.IsOn).IsFalse(); -} -``` - -### Authentication State - -```csharp -[Test] -public async Task Authentication_State() -{ - var authService = new AuthenticationService(); - - await Assert.That(authService.IsAuthenticated).IsFalse(); - - await authService.LoginAsync("user", "password"); - - await Assert.That(authService.IsAuthenticated).IsTrue(); -} -``` - -### Validation Scenarios - -```csharp -[Test] -public async Task Multiple_Validations() -{ - var form = new RegistrationForm - { - Email = "test@example.com", - Password = "SecurePass123!", - Age = 25 - }; - - using (Assert.Multiple()) - { - await Assert.That(form.IsEmailValid()).IsTrue(); - await Assert.That(form.IsPasswordStrong()).IsTrue(); - await Assert.That(form.IsAgeValid()).IsTrue(); - await Assert.That(form.IsComplete()).IsTrue(); - } -} -``` +Use `IsTrue()` / `IsFalse()` for actual boolean values and flags. For comparisons, collections, strings, and types, TUnit provides [dedicated assertions](collections.md) with better failure messages. ## See Also diff --git a/docs/docs/assertions/collections.md b/docs/docs/assertions/collections.md index a9a924b9e1..418195a397 100644 --- a/docs/docs/assertions/collections.md +++ b/docs/docs/assertions/collections.md @@ -799,25 +799,6 @@ public async Task Nested_Collections() } ``` -## Collection of Collections - -```csharp -[Test] -public async Task Collection_Of_Collections() -{ - var groups = new List> - { - new() { 1, 2 }, - new() { 3, 4, 5 }, - new() { 6 } - }; - - await Assert.That(groups) - .Count().IsEqualTo(3) - .And.All(group => group.Count > 0); -} -``` - ## Chaining Collection Assertions ```csharp @@ -918,21 +899,6 @@ public async Task Validate_All_With_Assertion() } ``` -### Find and Assert - -```csharp -[Test] -public async Task Find_And_Assert() -{ - var users = GetUsers(); - - var admin = await Assert.That(users) - .Contains(u => u.Role == "Admin"); - - await Assert.That(admin.Permissions).IsNotEmpty(); -} -``` - ## See Also - [Dictionaries](dictionaries.md) - Dictionary-specific assertions diff --git a/docs/docs/assertions/datetime.md b/docs/docs/assertions/datetime.md index 05c36d206f..660aff4253 100644 --- a/docs/docs/assertions/datetime.md +++ b/docs/docs/assertions/datetime.md @@ -433,23 +433,6 @@ public async Task Record_Created_Recently() } ``` -### Time Zone Conversions - -```csharp -[Test] -public async Task Time_Zone_Conversion() -{ - var utcTime = DateTime.UtcNow; - var localTime = utcTime.ToLocalTime(); - - await Assert.That(utcTime).IsUtc(); - await Assert.That(localTime).IsNotUtc(); - - var offset = localTime - utcTime; - await Assert.That(Math.Abs(offset.TotalHours)).IsLessThan(24); -} -``` - ## Working with Date Components ```csharp @@ -467,45 +450,6 @@ public async Task Date_Components() } ``` -## First and Last Day of Month - -```csharp -[Test] -public async Task First_Day_Of_Month() -{ - var date = new DateTime(2024, 3, 15); - var firstDay = new DateTime(date.Year, date.Month, 1); - - await Assert.That(firstDay.Day).IsEqualTo(1); -} - -[Test] -public async Task Last_Day_Of_Month() -{ - var date = new DateTime(2024, 2, 15); - var daysInMonth = DateTime.DaysInMonth(date.Year, date.Month); - var lastDay = new DateTime(date.Year, date.Month, daysInMonth); - - await Assert.That(lastDay.Day).IsEqualTo(29); // 2024 is a leap year -} -``` - -## Quarter Calculation - -```csharp -[Test] -public async Task Date_Quarter() -{ - var q1 = new DateTime(2024, 2, 1); - var quarter1 = (q1.Month - 1) / 3 + 1; - await Assert.That(quarter1).IsEqualTo(1); - - var q3 = new DateTime(2024, 8, 1); - var quarter3 = (q3.Month - 1) / 3 + 1; - await Assert.That(quarter3).IsEqualTo(3); -} -``` - ## DayOfWeek Assertions DayOfWeek has its own assertions: diff --git a/docs/docs/assertions/equality-and-comparison.md b/docs/docs/assertions/equality-and-comparison.md index f2b14bc868..6753884448 100644 --- a/docs/docs/assertions/equality-and-comparison.md +++ b/docs/docs/assertions/equality-and-comparison.md @@ -54,7 +54,7 @@ public async Task Using_EqualTo_Alias() var numbers = new[] { 1, 2, 3 }; await Assert.That(numbers) - .Count().IsEqualTo(3) + .Count().EqualTo(3) .And.Contains(2); } ``` @@ -282,8 +282,9 @@ public async Task Decimal_With_Tolerance() [Test] public async Task Long_With_Tolerance() { - long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); - long expected = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + long timestamp = now; + long expected = now + 50; // Simulate small drift // Allow 100ms difference await Assert.That(timestamp).IsEqualTo(expected).Within(100L); @@ -401,97 +402,6 @@ public async Task Struct_Equality() } ``` -## Practical Examples - -### Validating Calculation Results - -```csharp -[Test] -public async Task Calculate_Discount() -{ - var originalPrice = 100m; - var discount = 0.20m; // 20% - - var finalPrice = originalPrice * (1 - discount); - - await Assert.That(finalPrice).IsEqualTo(80m); - await Assert.That(finalPrice).IsLessThan(originalPrice); - await Assert.That(finalPrice).IsGreaterThan(0); -} -``` - -### Validating Ranges - -```csharp -[Test] -public async Task Temperature_In_Valid_Range() -{ - var roomTemperature = GetRoomTemperature(); - - await Assert.That(roomTemperature) - .IsBetween(18, 26) // Comfortable range in Celsius - .And.IsPositive(); -} -``` - -### Comparing with Mathematical Constants - -```csharp -[Test] -public async Task Mathematical_Constants() -{ - var calculatedPi = CalculatePiUsingLeibniz(10000); - - await Assert.That(calculatedPi).IsEqualTo(Math.PI).Within(0.0001); -} -``` - -### API Response Validation - -```csharp -[Test] -public async Task API_Response_Time() -{ - var stopwatch = Stopwatch.StartNew(); - await CallApiEndpoint(); - stopwatch.Stop(); - - await Assert.That(stopwatch.ElapsedMilliseconds) - .IsLessThan(500) // Must respond within 500ms - .And.IsGreaterThan(0); -} -``` - -## Common Patterns - -### Validating User Input - -```csharp -[Test] -public async Task Username_Length() -{ - var username = GetUserInput(); - - await Assert.That(username.Length) - .IsBetween(3, 20) - .And.IsGreaterThan(0); -} -``` - -### Percentage Validation - -```csharp -[Test] -public async Task Percentage_Valid() -{ - var successRate = CalculateSuccessRate(); - - await Assert.That(successRate) - .IsBetween(0, 100) - .And.IsGreaterThanOrEqualTo(0); -} -``` - ## See Also - [Numeric Assertions](numeric.md) - Additional numeric-specific assertions diff --git a/docs/docs/assertions/numeric.md b/docs/docs/assertions/numeric.md index ce38e4eb26..1f01278054 100644 --- a/docs/docs/assertions/numeric.md +++ b/docs/docs/assertions/numeric.md @@ -277,170 +277,6 @@ public async Task Validate_Score() } ``` -## Mathematical Operations - -### Addition - -```csharp -[Test] -public async Task Addition() -{ - var result = 5 + 3; - - await Assert.That(result).IsEqualTo(8); - await Assert.That(result).IsPositive(); - await Assert.That(result).IsGreaterThan(5); -} -``` - -### Subtraction - -```csharp -[Test] -public async Task Subtraction() -{ - var result = 10 - 3; - - await Assert.That(result).IsEqualTo(7); - await Assert.That(result).IsPositive(); -} -``` - -### Multiplication - -```csharp -[Test] -public async Task Multiplication() -{ - var result = 4 * 5; - - await Assert.That(result).IsEqualTo(20); - await Assert.That(result).IsPositive(); -} -``` - -### Division - -```csharp -[Test] -public async Task Division() -{ - double result = 10.0 / 4.0; - - await Assert.That(result).IsEqualTo(2.5).Within(0.001); - await Assert.That(result).IsPositive(); -} -``` - -### Modulo - -```csharp -[Test] -public async Task Modulo() -{ - var result = 17 % 5; - - await Assert.That(result).IsEqualTo(2); - await Assert.That(result).IsGreaterThanOrEqualTo(0); - await Assert.That(result).IsLessThan(5); -} -``` - -## Working with Math Library - -### Rounding - -```csharp -[Test] -public async Task Math_Round() -{ - double value = 3.7; - double rounded = Math.Round(value); - - await Assert.That(rounded).IsEqualTo(4.0).Within(0.001); -} -``` - -### Ceiling and Floor - -```csharp -[Test] -public async Task Math_Ceiling_Floor() -{ - double value = 3.2; - - double ceiling = Math.Ceiling(value); - await Assert.That(ceiling).IsEqualTo(4.0); - - double floor = Math.Floor(value); - await Assert.That(floor).IsEqualTo(3.0); -} -``` - -### Absolute Value - -```csharp -[Test] -public async Task Math_Abs() -{ - int negative = -42; - int positive = Math.Abs(negative); - - await Assert.That(positive).IsPositive(); - await Assert.That(positive).IsEqualTo(42); -} -``` - -### Power and Square Root - -```csharp -[Test] -public async Task Math_Power_Sqrt() -{ - double squared = Math.Pow(5, 2); - await Assert.That(squared).IsEqualTo(25.0).Within(0.001); - - double root = Math.Sqrt(25); - await Assert.That(root).IsEqualTo(5.0).Within(0.001); -} -``` - -### Trigonometry - -```csharp -[Test] -public async Task Math_Trigonometry() -{ - double angle = Math.PI / 4; // 45 degrees - double sine = Math.Sin(angle); - - await Assert.That(sine).IsEqualTo(Math.Sqrt(2) / 2).Within(0.0001); - await Assert.That(sine).IsPositive(); - await Assert.That(sine).IsBetween(0, 1); -} -``` - -## Increment and Decrement - -```csharp -[Test] -public async Task Increment_Decrement() -{ - int counter = 0; - - counter++; - await Assert.That(counter).IsEqualTo(1); - await Assert.That(counter).IsPositive(); - - counter--; - await Assert.That(counter).IsEqualTo(0); - - counter--; - await Assert.That(counter).IsEqualTo(-1); - await Assert.That(counter).IsNegative(); -} -``` - ## Chaining Numeric Assertions ```csharp diff --git a/docs/docs/benchmarks/AsyncTests.md b/docs/docs/benchmarks/AsyncTests.md index 2a414584f4..ecf70c39ff 100644 --- a/docs/docs/benchmarks/AsyncTests.md +++ b/docs/docs/benchmarks/AsyncTests.md @@ -12,7 +12,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI **Environment:** Ubuntu Latest • .NET SDK 10.0.103 ::: -## 📊 Results +## Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| @@ -22,7 +22,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI | xUnit3 | 3.2.2 | 783.8 ms | 783.0 ms | 7.77 ms | | **TUnit (AOT)** | 1.17.25 | 123.6 ms | 123.7 ms | 0.27 ms | -## 📈 Visual Comparison +## Visual Comparison ```mermaid %%{init: { @@ -62,7 +62,7 @@ xychart-beta bar [553.6, 724.3, 650, 783.8, 123.6] ``` -## 🎯 Key Insights +## Key Insights This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using identical test scenarios. diff --git a/docs/docs/benchmarks/BuildTime.md b/docs/docs/benchmarks/BuildTime.md index 1c72930388..2e3bb12f8c 100644 --- a/docs/docs/benchmarks/BuildTime.md +++ b/docs/docs/benchmarks/BuildTime.md @@ -12,7 +12,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI **Environment:** Ubuntu Latest • .NET SDK 10.0.103 ::: -## 📊 Results +## Results Compilation time comparison across frameworks: @@ -23,7 +23,7 @@ Compilation time comparison across frameworks: | Build_MSTest | 4.1.0 | 1.933 s | 1.932 s | 0.0186 s | | Build_xUnit3 | 3.2.2 | 1.839 s | 1.838 s | 0.0242 s | -## 📈 Visual Comparison +## Visual Comparison ```mermaid %%{init: { diff --git a/docs/docs/benchmarks/DataDrivenTests.md b/docs/docs/benchmarks/DataDrivenTests.md index 5171395fa5..6456a4561f 100644 --- a/docs/docs/benchmarks/DataDrivenTests.md +++ b/docs/docs/benchmarks/DataDrivenTests.md @@ -12,7 +12,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI **Environment:** Ubuntu Latest • .NET SDK 10.0.103 ::: -## 📊 Results +## Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| @@ -22,7 +22,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI | xUnit3 | 3.2.2 | 596.39 ms | 595.28 ms | 6.733 ms | | **TUnit (AOT)** | 1.17.25 | 25.26 ms | 25.27 ms | 0.808 ms | -## 📈 Visual Comparison +## Visual Comparison ```mermaid %%{init: { @@ -62,7 +62,7 @@ xychart-beta bar [468.83, 543.02, 539.01, 596.39, 25.26] ``` -## 🎯 Key Insights +## Key Insights This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using identical test scenarios. diff --git a/docs/docs/benchmarks/MassiveParallelTests.md b/docs/docs/benchmarks/MassiveParallelTests.md index 0bf4b953f5..01a291c4a3 100644 --- a/docs/docs/benchmarks/MassiveParallelTests.md +++ b/docs/docs/benchmarks/MassiveParallelTests.md @@ -12,7 +12,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI **Environment:** Ubuntu Latest • .NET SDK 10.0.103 ::: -## 📊 Results +## Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| @@ -22,7 +22,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI | xUnit3 | 3.2.2 | 3,123.1 ms | 3,127.7 ms | 19.41 ms | | **TUnit (AOT)** | 1.17.25 | 229.9 ms | 230.0 ms | 0.84 ms | -## 📈 Visual Comparison +## Visual Comparison ```mermaid %%{init: { @@ -62,7 +62,7 @@ xychart-beta bar [690.9, 1244.5, 2976.7, 3123.1, 229.9] ``` -## 🎯 Key Insights +## Key Insights This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using identical test scenarios. diff --git a/docs/docs/benchmarks/MatrixTests.md b/docs/docs/benchmarks/MatrixTests.md index 501e1c39d8..a07e39662c 100644 --- a/docs/docs/benchmarks/MatrixTests.md +++ b/docs/docs/benchmarks/MatrixTests.md @@ -12,7 +12,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI **Environment:** Ubuntu Latest • .NET SDK 10.0.103 ::: -## 📊 Results +## Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| @@ -22,7 +22,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI | xUnit3 | 3.2.2 | 1,601.8 ms | 1,599.1 ms | 11.90 ms | | **TUnit (AOT)** | 1.17.25 | 125.8 ms | 125.8 ms | 0.20 ms | -## 📈 Visual Comparison +## Visual Comparison ```mermaid %%{init: { @@ -62,7 +62,7 @@ xychart-beta bar [543.1, 1536.1, 1475.1, 1601.8, 125.8] ``` -## 🎯 Key Insights +## Key Insights This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using identical test scenarios. diff --git a/docs/docs/benchmarks/ScaleTests.md b/docs/docs/benchmarks/ScaleTests.md index 52a72afd37..8bfdaf82bb 100644 --- a/docs/docs/benchmarks/ScaleTests.md +++ b/docs/docs/benchmarks/ScaleTests.md @@ -12,7 +12,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI **Environment:** Ubuntu Latest • .NET SDK 10.0.103 ::: -## 📊 Results +## Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| @@ -22,7 +22,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI | xUnit3 | 3.2.2 | 653.14 ms | 652.57 ms | 9.228 ms | | **TUnit (AOT)** | 1.17.25 | 34.96 ms | 35.01 ms | 2.221 ms | -## 📈 Visual Comparison +## Visual Comparison ```mermaid %%{init: { @@ -62,7 +62,7 @@ xychart-beta bar [499.13, 634.78, 599.34, 653.14, 34.96] ``` -## 🎯 Key Insights +## Key Insights This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using identical test scenarios. diff --git a/docs/docs/benchmarks/SetupTeardownTests.md b/docs/docs/benchmarks/SetupTeardownTests.md index 4fc16c9de2..63d26a4480 100644 --- a/docs/docs/benchmarks/SetupTeardownTests.md +++ b/docs/docs/benchmarks/SetupTeardownTests.md @@ -12,7 +12,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI **Environment:** Ubuntu Latest • .NET SDK 10.0.103 ::: -## 📊 Results +## Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| @@ -22,7 +22,7 @@ This benchmark was automatically generated on **2026-02-26** from the latest CI | xUnit3 | 3.2.2 | 1,221.9 ms | 1,217.1 ms | 15.46 ms | | **TUnit (AOT)** | 1.17.25 | NA | NA | NA | -## 📈 Visual Comparison +## Visual Comparison ```mermaid %%{init: { @@ -62,7 +62,7 @@ xychart-beta bar [588.9, 1158.4, 1094.4, 1221.9, 0] ``` -## 🎯 Key Insights +## Key Insights This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using identical test scenarios. diff --git a/docs/docs/benchmarks/index.md b/docs/docs/benchmarks/index.md index 182b9cc083..71f1f7d8cd 100644 --- a/docs/docs/benchmarks/index.md +++ b/docs/docs/benchmarks/index.md @@ -12,7 +12,7 @@ These benchmarks were automatically generated on **2026-02-26** from the latest **Environment:** Ubuntu Latest • .NET SDK 10.0.103 ::: -## 🚀 Runtime Benchmarks +## Runtime Benchmarks Click on any benchmark to view detailed results: @@ -24,14 +24,14 @@ Click on any benchmark to view detailed results: - [SetupTeardownTests](SetupTeardownTests) - Detailed performance analysis -## 🔨 Build Benchmarks +## Build Benchmarks - [Build Performance](BuildTime) - Compilation time comparison --- -## 📊 Methodology +## Methodology These benchmarks compare TUnit against the most popular .NET testing frameworks: diff --git a/docs/docs/comparison/framework-differences.md b/docs/docs/comparison/framework-differences.md index 69f6a1aeb3..1c113adc6f 100644 --- a/docs/docs/comparison/framework-differences.md +++ b/docs/docs/comparison/framework-differences.md @@ -1,6 +1,6 @@ # Framework Differences -TUnit is inspired by NUnit and xUnit, and first and foremost I want to say that these are amazing frameworks and no hate to them. +TUnit is inspired by NUnit and xUnit, which are excellent frameworks that have served the .NET community well. **Why use TUnit?** TUnit aims to address some pain points and limitations found in other frameworks, especially around parallelism, lifecycle hooks, test isolation, and extensibility. @@ -57,8 +57,7 @@ public class ValidatorTests The TUnit package automatically configures global usings for common TUnit namespaces, so you don't need to include using statements in your test files. -So you'll be asking why use TUnit instead of them, right? -Here are some things I've stumbled across in the past that I've found limiting when writing a test suite. +The following sections describe specific limitations in other frameworks that TUnit addresses. ## xUnit @@ -74,7 +73,7 @@ Set ups and tear-downs work largely off of constructors and the `IDisposable` in There isn't a simplistic way to do something on starting an assembly's tests. For example, we might want to spin up 1 in-memory server to run some tests against. TUnit supports this with a simple static class, with a method containing the attribute `[Before(Assembly)]`. Tear down is as simple as another method with `[After(Assembly)]`. ### TestContext -Sometimes we want to access information about the state of a test. For example, when running UI tests, I like to take a screenshot on a test failure, so I can more easily see what went wrong. xUnit does not have a native way of determining if a test failed when you're in a tear down method. With TUnit, you can inject in a `TestContext` object into your tear down method, or you can call the static `TestContext.Current` static method. +Sometimes it is useful to access information about the state of a test. For example, when running UI tests, taking a screenshot on failure makes it easier to see what went wrong. xUnit does not have a native way of determining if a test failed when you're in a tear down method. With TUnit, you can inject in a `TestContext` object into your tear down method, or you can call the static `TestContext.Current` static method. ### Assertions xUnit assertions are fairly basic and have the problem of it being unclear which argument goes in which position, without sifting through intellisense/documentation. @@ -88,12 +87,12 @@ Assert.Equal(one, 1) ## NUnit ### Shared test class instances -This one has bitten me so many times, and I've seen it bite many others too. And a lot of people don't even know it. But the default behaviour of NUnit is to run all your tests within a class, against a single instance of that class. That means if you're storing state in fields/properties, they're going to be left over from previous tests. -This is what I call leaky test states, and I am firmly against it. Tests should be isolated from one another and really unable to affect one another. So TUnit by design runs every test against a new instance, and there is no way to change that because I consider it bad practice. If you want to share state in a field, then that's entirely possible by making it `static`. By utilising the language instead, it makes it clear to anyone reading it whether multiple tests can access that. +This is a common source of subtle bugs that many developers encounter without realizing it. The default behaviour of NUnit is to run all tests within a class against a single instance of that class. That means if state is stored in fields or properties, it persists from previous tests. +This pattern — sometimes called leaky test state — undermines test isolation. Tests should be independent and unable to affect one another. TUnit avoids this by design: every test runs against a new instance, with no way to opt out. To share state across tests, use `static` fields. This makes shared state explicit and immediately visible to anyone reading the code. ### Setting properties based off of dynamically injected data -I had a scenario in a multi-tenanted test suite where tests tests were repeated with different tenants injected in. -Like this: +Consider a multi-tenanted test suite where tests are repeated with different tenants injected in. +For example: ```csharp [TestFixtureSource(typeof(Tenant), nameof(Tenant.AllTenants))] public class MyTests(Tenant tenant) @@ -106,13 +105,13 @@ public class MyTests(Tenant tenant) } ``` -With this, I wanted to be able to filter by the tenant. So I tried using a custom attribute with `IApplyToTest` and setting a property based on the constructor argument. This didn't work. I think they're enumerated upon starting, and so you can't set this up beforehand. With TUnit, tests are enumerated and initialised via source-generation so this is all done up-front. So I could set a property in TUnit with an attribute with `ITestDiscoveryEvent`, set a property based constructor arguments, and then run `dotnet run --treenode-filter /*/*/*/*[Tenant=MyTenant]` +A natural next step is to filter by tenant. In NUnit, a custom attribute with `IApplyToTest` can set a property based on the constructor argument, but this does not work because tests are enumerated on start-up before the fixture source provides its values. In TUnit, tests are enumerated and initialised via source generation, so this is all done up-front. A property can be set with an attribute implementing `ITestDiscoveryEvent` based on constructor arguments, and then filtered with `dotnet run --treenode-filter /*/*/*/*[Tenant=MyTenant]`. ### Assembly & class level attributes Want to use the `[Repeat]` or `[Retry]` attributes on a class? Or even an assembly? You can't. They're only supported for test methods. With TUnit, most attributes are supported at Test, Class & Assembly levels. Test takes the highest priority, then class, then assembly. So you could set defaults with an assembly/class attribute, and then override it for certain tests by setting that same attribute on the test. ### Assertions -NUnit assertions largely influenced the way that TUnit assertions work. However, NUnit assertions do not have compile time checks. I could check if a string is negative (`NUnitAssert.That("String", Is.Negative);`) or if a boolean throws an exception (`NUnitAssert.That(true, Throws.ArgumentException);`). These assertions don't make sense. There are analyzers to help catch these - But they will compile if these analyzers aren't run. TUnit assertions are built with the type system in mind (where possible!). Specific assertions are built via extensions to the relevant types, and not in a generic sense that could apply to anything. That means when you're using intellisense to see what methods you have available, you should only see assertions that are relevant for your type. This makes it harder to make mistakes, and decreases your feedback loop time. +NUnit assertions largely influenced the way that TUnit assertions work. However, NUnit assertions do not have compile-time checks. Nothing prevents checking if a string is negative (`NUnitAssert.That("String", Is.Negative);`) or if a boolean throws an exception (`NUnitAssert.That(true, Throws.ArgumentException);`). These assertions do not make sense. There are analyzers to help catch these - But they will compile if these analyzers aren't run. TUnit assertions are built with the type system in mind (where possible!). Specific assertions are built via extensions to the relevant types, and not in a generic sense that could apply to anything. That means when you're using intellisense to see what methods you have available, you should only see assertions that are relevant for your type. This makes it harder to make mistakes, and decreases your feedback loop time. ## Other diff --git a/docs/docs/examples/playwright.md b/docs/docs/examples/playwright.md index c609117fef..ebcb0f6c58 100644 --- a/docs/docs/examples/playwright.md +++ b/docs/docs/examples/playwright.md @@ -24,9 +24,103 @@ The following properties are available to use: - `Browser` - `Playwright` -You can override the `BrowserName` to control which browser you want to launch. +## A Real Page Interaction Test -The possible values are: -- chromium -- firefox -- webkit +```csharp +public class LoginPageTests : PageTest +{ + [Test] + public async Task Login_Button_Is_Visible() + { + await Page.GotoAsync("https://example.com/login"); + + var loginButton = Page.Locator("button#login"); + + await Assert.That(await loginButton.IsVisibleAsync()).IsTrue(); + } + + [Test] + public async Task Successful_Login_Redirects_To_Dashboard() + { + await Page.GotoAsync("https://example.com/login"); + + await Page.FillAsync("#username", "testuser"); + await Page.FillAsync("#password", "password123"); + await Page.ClickAsync("button#login"); + + await Page.WaitForURLAsync("**/dashboard"); + + var heading = await Page.Locator("h1").TextContentAsync(); + + await Assert.That(heading).IsEqualTo("Dashboard"); + } +} +``` + +## Configuring Browser Options + +Override the `BrowserName` property to control which browser is launched. The possible values are: + +- `chromium` (default) +- `firefox` +- `webkit` + +Pass `BrowserTypeLaunchOptions` to the base constructor to configure headless mode, slow motion, and other launch settings: + +```csharp +public class HeadlessChromeTests : PageTest +{ + public HeadlessChromeTests() : base(new BrowserTypeLaunchOptions + { + Headless = true, + SlowMo = 100 // adds 100ms delay between actions for debugging + }) + { + } + + public override string BrowserName => "chromium"; + + [Test] + public async Task Page_Title_Matches() + { + await Page.GotoAsync("https://example.com"); + + var title = await Page.TitleAsync(); + + await Assert.That(title).Contains("Example"); + } +} +``` + +## Limiting Parallel Browser Tests + +Browser tests can be resource-intensive. Use `[ParallelLimiter]` to control how many run concurrently: + +```csharp +public class BrowserParallelLimit : IParallelLimit +{ + public int Limit => 2; +} + +[ParallelLimiter] +public class HeavyBrowserTests : PageTest +{ + [Test] + public async Task Test_A() + { + await Page.GotoAsync("https://example.com/a"); + await Assert.That(await Page.TitleAsync()).IsNotNull(); + } + + [Test] + public async Task Test_B() + { + await Page.GotoAsync("https://example.com/b"); + await Assert.That(await Page.TitleAsync()).IsNotNull(); + } +} +``` + +This ensures at most 2 tests from this class run at the same time, preventing browser resource exhaustion. + +For full Playwright API details, see the [Playwright for .NET documentation](https://playwright.dev/dotnet/). diff --git a/docs/docs/execution/ci-cd-reporting.md b/docs/docs/execution/ci-cd-reporting.md index e94dd0e33d..b779b76d7c 100644 --- a/docs/docs/execution/ci-cd-reporting.md +++ b/docs/docs/execution/ci-cd-reporting.md @@ -31,7 +31,7 @@ The collapsible style provides a clean, concise summary with expandable details: | 5 | Failed |
-📊 Test Details (click to expand) +Test Details (click to expand) ### Details | Test | Status | Details | Duration | diff --git a/docs/docs/execution/parallel-groups.md b/docs/docs/execution/parallel-groups.md index eec4d8bc99..8cbf8ca161 100644 --- a/docs/docs/execution/parallel-groups.md +++ b/docs/docs/execution/parallel-groups.md @@ -1,57 +1,80 @@ # Parallel Groups -Parallel Groups are an alternative parallel mechanism to [NotInParallel]. +Parallel groups control which tests can run at the same time. Classes that share the same `[ParallelGroup("key")]` are batched together: tests within the same group run in parallel with each other, but no tests from other groups run alongside them. The engine finishes one group entirely before starting the next. -Instead, classes that share a [ParallelGroup("")] attribute with the same key, may all run together in parallel, and nothing else will run alongside them. +## How It Works -For the example below, all `MyTestClass` tests may run in parallel, and all `MyTestClass2` tests may run in parallel. But they should not overlap and execute both classes at the same time. +Consider two groups: + +- **Group A**: `ClassA1` and `ClassA2` both have `[ParallelGroup("GroupA")]` +- **Group B**: `ClassB1` has `[ParallelGroup("GroupB")]` + +At runtime: +1. All tests from `ClassA1` and `ClassA2` run in parallel with each other. No other tests execute during this phase. +2. Once every test in Group A finishes, all tests from `ClassB1` run. No other tests execute during this phase. + +Tests that do not belong to any parallel group run separately, following the normal parallel execution rules. + +## Example ```csharp using TUnit.Core; namespace MyTestProject; -[ParallelGroup("Group1")] -public class MyTestClass +[ParallelGroup("Database")] +public class UserRepositoryTests { [Test] - public async Task MyTest() + public async Task Create_User() { - - } + var user = await UserRepository.CreateAsync("alice"); - [Test] - public async Task MyTest2() - { - + await Assert.That(user.Name).IsEqualTo("alice"); } [Test] - public async Task MyTest3() + public async Task Delete_User() { - + await UserRepository.DeleteAsync("bob"); + + var exists = await UserRepository.ExistsAsync("bob"); + + await Assert.That(exists).IsFalse(); } } -[ParallelGroup("Group2")] -public class MyTestClass2 +[ParallelGroup("Database")] +public class OrderRepositoryTests { [Test] - public async Task MyTest() + public async Task Create_Order() { - - } + var order = await OrderRepository.CreateAsync("item-1"); - [Test] - public async Task MyTest2() - { - + await Assert.That(order.Id).IsNotNull(); } +} +[ParallelGroup("ExternalApi")] +public class PaymentApiTests +{ [Test] - public async Task MyTest3() + public async Task Charge_Succeeds() { - + var result = await PaymentApi.ChargeAsync(100); + + await Assert.That(result.Success).IsTrue(); } } ``` + +`UserRepositoryTests` and `OrderRepositoryTests` share `"Database"`, so their tests all run in parallel with each other. `PaymentApiTests` is in `"ExternalApi"`, so it runs in a separate phase -- after the `"Database"` group finishes (or before, depending on scheduling order). + +## Difference from `[NotInParallel]` + +`[NotInParallel]` prevents individual tests from running at the same time as other tests that share the same constraint key. Each test with `[NotInParallel("Database")]` runs one at a time, sequentially. + +`[ParallelGroup("Database")]` allows tests within the group to run in parallel with each other. Only tests from *other* groups are excluded during that phase. + +Use `[NotInParallel]` when tests must never overlap. Use `[ParallelGroup]` when tests can safely overlap with each other but must be isolated from unrelated tests. diff --git a/docs/docs/execution/repeating.md b/docs/docs/execution/repeating.md index 502080fbea..a1b536341b 100644 --- a/docs/docs/execution/repeating.md +++ b/docs/docs/execution/repeating.md @@ -4,10 +4,16 @@ sidebar_position: 4 # Repeating -If you want to repeat a test, add a `[RepeatAttribute]` onto your test method or class. This takes an `int` of how many times you'd like to repeat. Each repeat will show in the test explorer as a new test. +If you want to repeat a test, add a `[Repeat]` attribute onto your test method or class. This takes an `int` of how many extra times the test should run. Each repeat appears in the test explorer as a separate test instance. This can be used on base classes and inherited to affect all tests in sub-classes. +## Repeat vs. Retry + +`[Repeat(N)]` always runs the test N additional times, unconditionally, regardless of pass or fail. `[Retry(N)]` only re-runs a test when it fails, up to N additional attempts, and stops as soon as it passes. Use `[Repeat]` for consistency and stress testing; use `[Retry]` for flaky test mitigation. + +## Example + ```csharp using TUnit.Core; @@ -17,13 +23,22 @@ public class MyTestClass { [Test] [Repeat(3)] - public async Task MyTest() + public async Task Calculation_Is_Consistent() { - + var result = Calculator.Add(2, 3); + + await Assert.That(result).IsEqualTo(5); } } ``` +This produces 4 test runs in total: the original plus 3 repeats. In the test explorer, they appear as: + +- `Calculation_Is_Consistent` +- `Calculation_Is_Consistent (RepeatIndex: 1)` +- `Calculation_Is_Consistent (RepeatIndex: 2)` +- `Calculation_Is_Consistent (RepeatIndex: 3)` + ## Global Repeat In case you want to apply the repeat logic to all tests in a project, you can add the attribute on the assembly level. diff --git a/docs/docs/execution/test-filters.md b/docs/docs/execution/test-filters.md index 2c19480dab..df8349c7b6 100644 --- a/docs/docs/execution/test-filters.md +++ b/docs/docs/execution/test-filters.md @@ -27,17 +27,78 @@ TUnit supports several operators for building complex filters: For full information on the treenode filters, see [Microsoft's documentation](https://github.com/microsoft/testfx/blob/main/docs/mstest-runner-graphqueryfiltering/graph-query-filtering.md) -So an example could be: +## Examples -`dotnet run --treenode-filter /*/*/LoginTests/*` - To run all tests in the class `LoginTests` +### Filter by class name -or +Run all tests in the `LoginTests` class: -`dotnet run --treenode-filter /*/*/*/AcceptCookiesTest` - To run all tests with the name `AcceptCookiesTest` +```bash +dotnet run --treenode-filter "/*/*/LoginTests/*" +``` -TUnit also supports filtering by your own [properties](../writing-tests/test-context.md#custom-properties). So you could do: +### Filter by test name -`dotnet run --treenode-filter /*/*/*/*[MyFilterName=*SomeValue*]` +Run all tests with the name `AcceptCookiesTest`: -And if your test had a property with the name "MyFilterName" and its value contained "SomeValue", then your test would be executed. +```bash +dotnet run --treenode-filter "/*/*/*/AcceptCookiesTest" +``` +### Filter by namespace + +Run all tests in a specific namespace: + +```bash +dotnet run --treenode-filter "/*/MyProject.Tests.Integration/*/*" +``` + +Use a wildcard to match namespace prefixes: + +```bash +dotnet run --treenode-filter "/*/MyProject.Tests.Api*/*/*" +``` + +### Filter by custom property value + +TUnit supports filtering by [custom properties](../writing-tests/test-context.md#custom-properties). If a test has a property with a matching name and value, it will be included: + +```bash +dotnet run --treenode-filter "/*/*/*/*[Category=Smoke]" +``` + +Use a wildcard to match partial property values: + +```bash +dotnet run --treenode-filter "/*/*/*/*[Owner=*Team-Backend*]" +``` + +### Exclude tests by property + +Use `!=` to exclude tests with a specific property value: + +```bash +dotnet run --treenode-filter "/*/*/*/*[Category!=Slow]" +``` + +### Combined filters + +Combine multiple conditions with `&` to narrow results. Run only high-priority smoke tests: + +```bash +dotnet run --treenode-filter "/*/*/*/*[Category=Smoke]&[Priority=High]" +``` + +Combine namespace and property filters. Run integration tests tagged as critical: + +```bash +dotnet run --treenode-filter "/*/MyProject.Tests.Integration/*/*/*[Priority=Critical]" +``` + +### OR filter across classes + +Run tests from either of two classes: + +```bash +dotnet run --treenode-filter "/*/*/(LoginTests)|(SignupTests)/*" +``` diff --git a/docs/docs/execution/timeouts.md b/docs/docs/execution/timeouts.md index e2bb59fb89..af60dd639f 100644 --- a/docs/docs/execution/timeouts.md +++ b/docs/docs/execution/timeouts.md @@ -6,10 +6,14 @@ sidebar_position: 5 If you want to stop a test after a specified amount of time, add a `[Timeout]` attribute onto your test method or class. This takes an `int` of how many milliseconds a test can execute for. -A cancellation token will be passed to tests too, which should be used where appropriate. This ensures that after the timeout is reached, operations are cancelled properly, and not wasting system resources. +When the timeout is exceeded, the test fails and the `CancellationToken` is cancelled. Any operations using the token will be aborted, preventing wasted system resources on a test that has already failed. This can be used on base classes and inherited to affect all tests in sub-classes. +## Example + +Pass the `CancellationToken` parameter to your test method to receive the timeout-linked token. Forward it to any async operations: + ```csharp using TUnit.Core; @@ -19,25 +23,47 @@ public class MyTestClass { [Test] [Timeout(30_000)] - public async Task MyTest(CancellationToken cancellationToken) + public async Task Api_Responds_Within_30_Seconds(CancellationToken cancellationToken) { - + using var client = new HttpClient(); + + var response = await client.GetAsync("https://api.example.com/health", cancellationToken); + + await Assert.That(response.IsSuccessStatusCode).IsTrue(); } } ``` +If the HTTP call takes longer than 30 seconds, `cancellationToken` is cancelled, the `GetAsync` call throws an `OperationCanceledException`, and the test is reported as failed due to timeout. + +## Timeout and Retries + +When a test has both `[Timeout]` and `[Retry]`, each retry attempt gets its own fresh timeout. If the first attempt times out at 5 seconds, the retry starts from zero with a new 5-second window: + +```csharp +[Test] +[Timeout(5_000)] +[Retry(2)] +public async Task Flaky_Service_Call(CancellationToken cancellationToken) +{ + var result = await FlakyService.CallAsync(cancellationToken); + + await Assert.That(result.Status).IsEqualTo("OK"); +} +``` + ## Global Timeout In case you want to apply the timeout to all tests in a project, you can add the attribute on the assembly level. ```csharp -[assembly: Timeout(3000)] +[assembly: Timeout(30_000)] ``` Or you can apply the Timeout on all the tests in a class like this: ```csharp -[Timeout(3000)] +[Timeout(30_000)] public class MyTestClass { } diff --git a/docs/docs/extending/built-in-extensions.md b/docs/docs/extending/built-in-extensions.md index bba6f53af3..9c4ebc8636 100644 --- a/docs/docs/extending/built-in-extensions.md +++ b/docs/docs/extending/built-in-extensions.md @@ -32,7 +32,7 @@ See the [Code Coverage](code-coverage.md) page for usage, configuration, and CI/ TRX reports are provided via the `Microsoft.Testing.Extensions.TrxReport` NuGet package. -**✅ Included automatically with the TUnit package** - No manual installation needed! +**Included automatically with the TUnit package** - No manual installation needed. #### Usage @@ -45,7 +45,7 @@ dotnet run --configuration Release --report-trx dotnet run --configuration Release --results-directory ./reports --report-trx --report-trx-filename testresults.trx ``` -**📚 More Resources:** +**More Resources:** - [Microsoft's TRX Report Documentation](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-platform-extensions-test-reports) --- @@ -54,7 +54,7 @@ dotnet run --configuration Release --results-directory ./reports --report-trx -- Telemetry is provided via the `Microsoft.Testing.Extensions.Telemetry` NuGet package. -**✅ Included automatically with the TUnit package** +**Included automatically with the TUnit package** This extension enables Microsoft to collect anonymous usage metrics to help improve the testing platform. No personal data or source code is collected. @@ -72,7 +72,7 @@ set TESTINGPLATFORM_TELEMETRY_OPTOUT=1 Alternatively, you can use `DOTNET_CLI_TELEMETRY_OPTOUT=1` which also disables .NET SDK telemetry. -**📚 More Resources:** +**More Resources:** - [Microsoft.Testing.Platform Telemetry Documentation](https://learn.microsoft.com/en-us/dotnet/core/testing/microsoft-testing-platform-telemetry) --- diff --git a/docs/docs/extending/libraries.md b/docs/docs/extending/libraries.md index 98c7f44a85..ec97905b6e 100644 --- a/docs/docs/extending/libraries.md +++ b/docs/docs/extending/libraries.md @@ -1,5 +1,88 @@ # Libraries -If you want a library package to define things like re-useable base classes with hooks etc, then you shouldn't use the main `TUnit` package - As this assumes your project is a test project and tries to build it as an executable etc. +When building a reusable library that defines shared hooks, custom attributes, base classes, or data sources for TUnit, reference **`TUnit.Core`** instead of the main `TUnit` package. The `TUnit` package configures a project as an executable test suite; `TUnit.Core` provides all the models and attributes needed for authoring test infrastructure without the test runner wiring. -Instead, reference `TUnit.Core` instead - It has all of the models required for wiring up your tests, but without all the extra setting up of the test suite execution. +## When to Build a TUnit Library + +- Shared lifecycle hooks (e.g., database setup, authentication) used across multiple test projects +- Custom attributes that encapsulate common test metadata or behavior +- Reusable data sources for parameterized tests +- Base test classes with standard setup and teardown logic + +## Example `.csproj` for a TUnit Library + +```xml + + + + net8.0;net9.0;net10.0 + + + + + + + +``` + +This produces a class library (`.dll`), not an executable. + +## Example Library Code + +```csharp +using TUnit.Core; + +namespace MyCompany.Testing; + +public abstract class DatabaseTestBase +{ + [Before(HookType.Test)] + public async Task ResetDatabase() + { + await TestDatabase.ResetAsync(); + } + + [After(HookType.Test)] + public async Task CleanupConnections() + { + await TestDatabase.CloseConnectionsAsync(); + } +} +``` + +## Consuming the Library + +In a test project, reference both `TUnit` (for the runner) and the library project: + +```xml + + + + net8.0;net9.0;net10.0 + Exe + + + + + + + + +``` + +Tests then inherit from the shared base class: + +```csharp +using MyCompany.Testing; + +public class OrderTests : DatabaseTestBase +{ + [Test] + public async Task Order_Is_Created_Successfully() + { + var order = await OrderService.CreateAsync("item-1"); + + await Assert.That(order.Id).IsNotNull(); + } +} +``` diff --git a/docs/docs/getting-started/installation.md b/docs/docs/getting-started/installation.md index e41ed3665b..613e962eb0 100644 --- a/docs/docs/getting-started/installation.md +++ b/docs/docs/getting-started/installation.md @@ -54,16 +54,16 @@ public class MyTests // No [TestClass] needed! When you install the **TUnit** meta package, you automatically get several useful extensions without any additional installation: -#### ✅ Built-In Extensions +#### Built-In Extensions **Microsoft.Testing.Extensions.CodeCoverage** -- 📊 Code coverage support via `--coverage` flag -- 📈 Outputs Cobertura and XML formats -- 🔄 Replacement for Coverlet (which is **not compatible** with TUnit) +- Code coverage support via `--coverage` flag +- Outputs Cobertura and XML formats +- Replacement for Coverlet (which is **not compatible** with TUnit) **Microsoft.Testing.Extensions.TrxReport** -- 📝 TRX test report generation via `--report-trx` flag -- 🤝 Compatible with Azure DevOps and other CI/CD systems +- TRX test report generation via `--report-trx` flag +- Compatible with Azure DevOps and other CI/CD systems This means you can run tests with coverage and reports right away: diff --git a/docs/docs/guides/cookbook.md b/docs/docs/guides/cookbook.md index 4dd452d635..f8b12f388f 100644 --- a/docs/docs/guides/cookbook.md +++ b/docs/docs/guides/cookbook.md @@ -98,32 +98,13 @@ public class OrderServiceTests ### Testing a Minimal API Endpoint (Shared Server) -For API tests, it's more efficient to share a single WebApplicationFactory across all tests: +For API tests, share a single WebApplicationFactory across all tests. See [Best Practices -- Sharing Expensive Resources](best-practices.md#sharing-expensive-resources) for the shared `TestWebServer` pattern using `ClassDataSource` with `SharedType.PerTestSession`. ```csharp -using Microsoft.AspNetCore.Mvc.Testing; using System.Net; using System.Net.Http.Json; using TUnit.Core; -// Shared web server for all API tests -public class TestWebServer : IAsyncInitializer, IAsyncDisposable -{ - public WebApplicationFactory? Factory { get; private set; } - - public async Task InitializeAsync() - { - Factory = new WebApplicationFactory(); - await Task.CompletedTask; - } - - public async ValueTask DisposeAsync() - { - if (Factory != null) - await Factory.DisposeAsync(); - } -} - [ClassDataSource(Shared = SharedType.PerTestSession)] public class UserApiTests(TestWebServer server) { diff --git a/docs/docs/guides/philosophy.md b/docs/docs/guides/philosophy.md index 957cb94280..e5c4295159 100644 --- a/docs/docs/guides/philosophy.md +++ b/docs/docs/guides/philosophy.md @@ -170,14 +170,6 @@ TUnit is a good fit when performance matters—you have large test suites that n When might you want alternatives? If you have an existing huge test suite, migration costs might outweigh the benefits. If your team strongly prefers another framework's style, that's a legitimate reason to stick with what works for you. Or if you absolutely need a tool that only works with VSTest, you'll need to use something else. -## The Bottom Line - -TUnit exists because modern .NET deserves a modern testing framework. One that prioritizes performance, isolation, and developer experience without carrying the baggage of legacy compromises. - -Every decision—async assertions, parallel-by-default, source generation—flows from wanting tests to be fast, isolated, modern, and pleasant to write. Tests should run in parallel, create new instances per test, support async naturally, and minimize boilerplate. - -If that resonates with you, TUnit is probably a good fit for your project. - For migration details, check out: - [xUnit Migration](../migration/xunit.md) - [NUnit Migration](../migration/nunit.md) diff --git a/docs/docs/reference/test-configuration.md b/docs/docs/reference/test-configuration.md index 8bcb7acd2a..8b4d8547b4 100644 --- a/docs/docs/reference/test-configuration.md +++ b/docs/docs/reference/test-configuration.md @@ -2,14 +2,15 @@ TUnit supports having a `testconfig.json` file within your test project. -This can be used to store key-value configuration pairs. +This can be used to store key-value configuration pairs. To retrieve these within tests, use the static method `TestContext.Configuration.Get(key)`. -To retrieve these within your tests, you can use the static method `TestContext.Configuration.Get(key)` +## Example `testconfig.json` ```json { "MyKey1": "MyValue1", + "BaseUrl": "https://api.example.com", "Nested": { "MyKey2": "MyValue2" } @@ -18,13 +19,59 @@ To retrieve these within your tests, you can use the static method `TestContext. `Tests.cs` ```csharp - [Test] - public async Task Test() - { - var value1 = TestContext.Configuration.Get("MyKey1"); // MyValue1 - As defined above - var value2 = TestContext.Configuration.Get("Nested:MyKey2"); // MyValue2 - As defined above - - ... +[Test] +public async Task Test() +{ + var value1 = TestContext.Configuration.Get("MyKey1"); // "MyValue1" + var value2 = TestContext.Configuration.Get("Nested:MyKey2"); // "MyValue2" + + await Assert.That(value1).IsEqualTo("MyValue1"); +} +``` + +## Missing Keys and Files + +If a key does not exist, `Get` returns `null`. If the `testconfig.json` file is missing entirely, all calls to `Get` return `null`. There is no exception thrown in either case. + +```csharp +[Test] +public async Task Configuration_Returns_Null_For_Unknown_Key() +{ + var value = TestContext.Configuration.Get("DoesNotExist"); + + await Assert.That(value).IsNull(); +} +``` + +## Nested Key Syntax + +Use a colon (`:`) to access values nested inside JSON objects. The path follows the same convention as `Microsoft.Extensions.Configuration`: + +```json +{ + "Database": { + "Connection": { + "Timeout": "30" } + } +} +``` + +```csharp +var timeout = TestContext.Configuration.Get("Database:Connection:Timeout"); // "30" ``` +## Typed Configuration + +All values are returned as `string?`. Convert to the required type as needed: + +```csharp +[Test] +public async Task Respects_Configured_Timeout() +{ + var rawTimeout = TestContext.Configuration.Get("Database:Connection:Timeout"); + var timeout = int.Parse(rawTimeout!); + + await Assert.That(timeout).IsEqualTo(30); +} +``` diff --git a/docs/docs/writing-tests/explicit.md b/docs/docs/writing-tests/explicit.md index a25a9fc784..3c82d80a3b 100644 --- a/docs/docs/writing-tests/explicit.md +++ b/docs/docs/writing-tests/explicit.md @@ -1,17 +1,14 @@ # Explicit -If you want a test to only be run explicitly (and not part of all general tests) then you can add the `[ExplicitAttribute]`. +If you want a test to only be run explicitly (and not part of all general tests) then you can add the `[Explicit]` attribute. This can be added to a test method or a test class. -A test is considered 'explicitly' run when all filtered tests have an explicit attribute on them. +A test is considered 'explicitly' run when all filtered tests have an explicit attribute on them. That means that you could run all tests in a class with an `[Explicit]` attribute. Or you could run a single method with an `[Explicit]` attribute. But if you try to run a mix of explicit and non-explicit tests, then the ones with an `[Explicit]` attribute will be excluded from the run. -This can be useful for 'Tests' that make sense in a local environment, and maybe not part of your CI builds. Or they could be helpers that ping things to warm them up, and by making them explicit tests, they are easily runnable, but don't affect your overall test suite. - -> **Tip:** -> To run explicit tests, use a filter to select only those tests (e.g., by name or category), or run them directly from your IDE's test explorer. +This can be useful for tests that make sense in a local environment but not as part of CI builds, or for helper utilities that should be easily runnable without affecting the overall test suite. ```csharp using TUnit.Core; @@ -22,10 +19,56 @@ public class MyTestClass { [Test] [Explicit] - public async Task MyTest() + public async Task Seed_Local_Database() { - + await Database.SeedAsync(); + + await Assert.That(await Database.CountAsync()).IsGreaterThan(0); } } ``` +## Running Explicit Tests from the Command Line + +Use a treenode filter that selects only the explicit test by name or class: + +```bash +dotnet run -- --treenode-filter "/*/*/MyTestClass/Seed_Local_Database" +``` + +Because every test matched by the filter has `[Explicit]`, TUnit will run them. + +## Combining `[Explicit]` with `[Category]` + +Use `[Category]` to group explicit tests so they can be filtered by property: + +```csharp +public class DevUtilities +{ + [Test] + [Explicit] + [Category("DevTool")] + public async Task Warm_Up_Cache() + { + await CacheService.WarmUpAsync(); + + await Assert.That(CacheService.IsWarmed).IsTrue(); + } + + [Test] + [Explicit] + [Category("DevTool")] + public async Task Reset_Feature_Flags() + { + await FeatureFlags.ResetAllAsync(); + + await Assert.That(await FeatureFlags.CountAsync()).IsEqualTo(0); + } +} +``` + +Run all explicit dev tools at once: + +```bash +dotnet run -- --treenode-filter "/*/*/*/*[Category=DevTool]" +``` diff --git a/docs/docs/writing-tests/generic-attributes.md b/docs/docs/writing-tests/generic-attributes.md index 469f5ed2e5..6383ed63fd 100644 --- a/docs/docs/writing-tests/generic-attributes.md +++ b/docs/docs/writing-tests/generic-attributes.md @@ -190,40 +190,22 @@ public abstract class TypedDataSourceAttribute : DataSourceAttribute public abstract IEnumerable GetData(); } -// Implementation example -public class FibonacciDataAttribute : TypedDataSourceAttribute +// Custom implementation +public class SampleUsersAttribute : TypedDataSourceAttribute { - private readonly int _count; - - public FibonacciDataAttribute(int count) + public override IEnumerable GetData() { - _count = count; - } - - public override IEnumerable GetData() - { - int a = 0, b = 1; - yield return a; - - if (_count > 1) yield return b; - - for (int i = 2; i < _count; i++) - { - int temp = a + b; - yield return temp; - a = b; - b = temp; - } + yield return new User { Id = 1, Name = "Alice", Role = "Admin" }; + yield return new User { Id = 2, Name = "Bob", Role = "User" }; } } // Usage [Test] -[FibonacciData(7)] -public void TestFibonacciNumber(int fibNumber) +[SampleUsers] +public async Task ValidateUser(User user) { - // Test with Fibonacci sequence: 0, 1, 1, 2, 3, 5, 8 - Assert.That(fibNumber).IsGreaterThanOrEqualTo(0); + await Assert.That(user.Name).IsNotEmpty(); } ``` @@ -412,30 +394,6 @@ public class ReflectiveDataSource<[DynamicallyAccessedMembers( } ``` -### Generic Constraints for AOT - -Use constraints to ensure AOT compatibility: - -```csharp -public class SerializableDataSource : TypedDataSourceAttribute - where T : IJsonSerializable // Ensures T can be serialized -{ - private readonly string _jsonFile; - - public SerializableDataSource(string jsonFile) - { - _jsonFile = jsonFile; - } - - public override IEnumerable GetData() - { - var json = File.ReadAllText(_jsonFile); - var items = JsonSerializer.Deserialize>(json); - return items ?? Enumerable.Empty(); - } -} -``` - ## Best Practices ### 1. Use Generic Attributes for Type Safety @@ -498,81 +456,6 @@ public class CsvDataSource : TypedDataSourceAttribute } ``` -## Common Patterns - -### Factory Pattern with Generics - -```csharp -public class EntityFactory where T : IEntity, new() -{ - public static IEnumerable CreateTestEntities(int count) - { - for (int i = 0; i < count; i++) - { - yield return new T - { - Id = i, - CreatedAt = DateTime.UtcNow - }; - } - } -} - -public class FactoryDataSource : TypedDataSourceAttribute - where T : IEntity, new() -{ - private readonly int _count; - - public FactoryDataSource(int count = 3) - { - _count = count; - } - - public override IEnumerable GetData() - { - return EntityFactory.CreateTestEntities(_count); - } -} - -// Usage -[Test] -[FactoryDataSource(5)] -public async Task TestProductEntity(Product product) -{ - await Assert.That(product.Id).IsGreaterThanOrEqualTo(0); -} -``` - -### Builder Pattern with Generics - -```csharp -public abstract class TestDataBuilder : TypedDataSourceAttribute -{ - protected abstract T BuildDefault(); - protected abstract T BuildInvalid(); - protected abstract T BuildEdgeCase(); - - public override IEnumerable GetData() - { - yield return BuildDefault(); - yield return BuildInvalid(); - yield return BuildEdgeCase(); - } -} - -public class UserDataBuilder : TestDataBuilder -{ - protected override User BuildDefault() => - new User { Id = 1, Name = "John", Age = 30 }; - - protected override User BuildInvalid() => - new User { Id = -1, Name = "", Age = -5 }; - - protected override User BuildEdgeCase() => - new User { Id = int.MaxValue, Name = new string('a', 1000), Age = 150 }; -} -``` - ## Summary Generic attributes in TUnit provide: diff --git a/docs/docs/writing-tests/method-data-source.md b/docs/docs/writing-tests/method-data-source.md index 1342a9e699..7e0ec316da 100644 --- a/docs/docs/writing-tests/method-data-source.md +++ b/docs/docs/writing-tests/method-data-source.md @@ -23,81 +23,9 @@ Returning a `Func` ensures that each test gets a fresh object. If you return a reference to the same object, tests may interfere with each other. ::: -Here's an example returning a simple object: +Return an `IEnumerable>` to generate multiple test cases. For each item returned, a new test will be created with that item passed in to the parameters. Each `Func<>` should return a `new T()` so every test gets its own instance. -```csharp -using TUnit.Assertions; -using TUnit.Assertions.Extensions; -using TUnit.Core; - -namespace MyTestProject; - -public record AdditionTestData(int Value1, int Value2, int ExpectedResult); - -public static class MyTestDataSources -{ - public static Func AdditionTestData() - { - return () => new AdditionTestData(1, 2, 3); - } -} - -public class MyTestClass -{ - [Test] - [MethodDataSource(typeof(MyTestDataSources), nameof(MyTestDataSources.AdditionTestData))] - public async Task MyTest(AdditionTestData additionTestData) - { - var result = Add(additionTestData.Value1, additionTestData.Value2); - - await Assert.That(result).IsEqualTo(additionTestData.ExpectedResult); - } - - private int Add(int x, int y) - { - return x + y; - } -} -``` - -This can also accept tuples if you don't want to create lots of new types within your test assembly: - -```csharp -using TUnit.Assertions; -using TUnit.Assertions.Extensions; -using TUnit.Core; - -namespace MyTestProject; - -public static class MyTestDataSources -{ - public static Func<(int, int, int)> AdditionTestData() - { - return () => (1, 2, 3); - } -} - -public class MyTestClass -{ - [Test] - [MethodDataSource(typeof(MyTestDataSources), nameof(MyTestDataSources.AdditionTestData))] - public async Task MyTest(int value1, int value2, int expectedResult) - { - var result = Add(value1, value2); - - await Assert.That(result).IsEqualTo(expectedResult); - } - - private int Add(int x, int y) - { - return x + y; - } -} -``` - -This attribute can also accept `IEnumerable<>`. For each item returned, a new test will be created with that item passed in to the parameters. Again, if using a reference type, return an `IEnumerable>` and make sure each `Func<>` returns a `new T()` - -Here's an example where the test would be invoked 3 times: +Here's an example using a record type (the test is invoked 3 times): ```csharp using TUnit.Assertions; diff --git a/docs/docs/writing-tests/property-injection.md b/docs/docs/writing-tests/property-injection.md index 691af95f2f..5abc02aceb 100644 --- a/docs/docs/writing-tests/property-injection.md +++ b/docs/docs/writing-tests/property-injection.md @@ -322,102 +322,15 @@ public class InMemorySql : IAsyncInitializer, IAsyncDisposable } } -// Redis container with similar pattern -public class InMemoryRedis : IAsyncInitializer, IAsyncDisposable -{ - private TestcontainersContainer? _container; - - public TestcontainersContainer Container => _container - ?? throw new InvalidOperationException("Container not initialized"); - - public async Task InitializeAsync() - { - _container = new TestcontainersBuilder() - .WithImage("redis:latest") - .Build(); - - await _container.StartAsync(); - } - - public async ValueTask DisposeAsync() - { - if (_container != null) - { - await _container.DisposeAsync(); - } - } -} - -// Message bus container -public class InMemoryMessageBus : IAsyncInitializer, IAsyncDisposable -{ - private TestcontainersContainer? _container; - - public TestcontainersContainer Container => _container - ?? throw new InvalidOperationException("Container not initialized"); - - public async Task InitializeAsync() - { - _container = new TestcontainersBuilder() - .WithImage("rabbitmq:3-management") - .Build(); - - await _container.StartAsync(); - } +// Apply the same pattern for other services (Redis, message buses, etc.) - public async ValueTask DisposeAsync() - { - if (_container != null) - { - await _container.DisposeAsync(); - } - } -} - -// UI component that depends on the message bus -public class MessageBusUserInterface : IAsyncInitializer, IAsyncDisposable -{ - private TestcontainersContainer? _container; - - // Inject the message bus dependency - shared per test session - [ClassDataSource(Shared = SharedType.PerTestSession)] - public required InMemoryMessageBus MessageBus { get; init; } - - public TestcontainersContainer Container => _container - ?? throw new InvalidOperationException("Container not initialized"); - - public async Task InitializeAsync() - { - // The MessageBus property is already initialized when this runs! - _container = new MessageBusUIContainerBuilder() - .WithConnectionString(MessageBus.Container.GetConnectionString()) - .Build(); - - await _container.StartAsync(); - } - - public async ValueTask DisposeAsync() - { - if (_container != null) - { - await _container.DisposeAsync(); - } - } -} - -// Web application factory that depends on multiple services +// Web application factory that depends on infrastructure services public class InMemoryWebApplicationFactory : WebApplicationFactory, IAsyncInitializer { - // Inject all required infrastructure - all shared per test session + // Inject required infrastructure - shared per test session [ClassDataSource(Shared = SharedType.PerTestSession)] public required InMemorySql Sql { get; init; } - [ClassDataSource(Shared = SharedType.PerTestSession)] - public required InMemoryRedis Redis { get; init; } - - [ClassDataSource(Shared = SharedType.PerTestSession)] - public required InMemoryMessageBus MessageBus { get; init; } - public Task InitializeAsync() { // Force server creation to validate configuration @@ -429,11 +342,9 @@ public class InMemoryWebApplicationFactory : WebApplicationFactory, IAs { builder.ConfigureAppConfiguration((context, configBuilder) => { - // All injected properties are already initialized! + // Injected properties are already initialized! configBuilder.AddInMemoryCollection(new Dictionary { - { "MessageBus:ConnectionString", MessageBus.Container.GetConnectionString() }, - { "Redis:ConnectionString", Redis.Container.GetConnectionString() }, { "PostgreSql:ConnectionString", Sql.Container.GetConnectionString() } }); }); @@ -447,21 +358,14 @@ public class IntegrationTests [ClassDataSource] public required InMemoryWebApplicationFactory WebApplicationFactory { get; init; } - [ClassDataSource] - public required MessageBusUserInterface MessageBusUI { get; init; } - [Test] public async Task Full_Integration_Test() { // Everything is initialized in the correct order! var client = WebApplicationFactory.CreateClient(); - // Test your application with all infrastructure running var response = await client.GetAsync("/api/products"); await Assert.That(response.IsSuccessStatusCode).IsTrue(); - - // The MessageBusUI shares the same MessageBus instance as the WebApplicationFactory - // because they both use SharedType.PerTestSession } } ``` @@ -530,5 +434,4 @@ public class ServiceB : IAsyncInitializer } ``` -This powerful feature makes complex test orchestration simple and maintainable, allowing you to focus on writing tests rather than managing test infrastructure! diff --git a/docs/docs/writing-tests/things-to-know.md b/docs/docs/writing-tests/things-to-know.md index 07f6023931..f9f6916e08 100644 --- a/docs/docs/writing-tests/things-to-know.md +++ b/docs/docs/writing-tests/things-to-know.md @@ -12,7 +12,7 @@ Classes are `new`ed up for each test within their class. This is by design because tests should be stateless and side effect free. -By doing this it enables parallelisation (for speed and throughput), and reduces bugs and side effects when there is stale data left over from previous tests. This is something I've experienced with NUnit before. I've seen test suites that were all green, and they were actually broken, because they were asserting against instance data that had been left over from previous tests. +By doing this it enables parallelisation (for speed and throughput), and reduces bugs and side effects when there is stale data left over from previous tests. Test suites can appear green while actually being broken, because they assert against instance data left over from previous tests. So if you have: From 060c3fa77cf09c11b95698a5aa855c3a96cb0785 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:38:08 +0000 Subject: [PATCH 6/9] docs: fix broken links from directory restructure - Fix cookbook.md link to deleted examples/intro.md - Fix artifacts.md, hooks-cleanup.md, lifecycle.md links to renamed setup/cleanup files - Fix ChooseYourJourney component links to new directory paths Build passes with zero broken links. --- docs/docs/guides/cookbook.md | 2 +- docs/docs/writing-tests/artifacts.md | 2 +- docs/docs/writing-tests/hooks-cleanup.md | 2 +- docs/docs/writing-tests/lifecycle.md | 4 ++-- docs/src/components/ChooseYourJourney/index.tsx | 10 +++++----- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/docs/guides/cookbook.md b/docs/docs/guides/cookbook.md index f8b12f388f..149ce3f050 100644 --- a/docs/docs/guides/cookbook.md +++ b/docs/docs/guides/cookbook.md @@ -774,4 +774,4 @@ These cookbook recipes cover the most common testing scenarios. You can adapt th - **Exception Testing**: Use TUnit's fluent exception assertions - **Integration Tests**: Test with real databases, containers, or file systems -For more examples, check out the [examples section](../examples/intro) in the documentation. +For more examples, check out the [ASP.NET Core](../examples/aspnet) and [Aspire](../examples/aspire) integration guides. diff --git a/docs/docs/writing-tests/artifacts.md b/docs/docs/writing-tests/artifacts.md index 4a2cc6bfcc..71cce9a3db 100644 --- a/docs/docs/writing-tests/artifacts.md +++ b/docs/docs/writing-tests/artifacts.md @@ -446,5 +446,5 @@ The exact behavior depends on your test runner configuration and CI/CD platform. ## See Also - [Test Context](./test-context.md) - Overview of TestContext -- [Test Lifecycle Hooks](./setup.md) - Using Before/After hooks +- [Test Lifecycle Hooks](./hooks-setup.md) - Using Before/After hooks - [CI/CD Reporting](../execution/ci-cd-reporting.md) - Integrating with CI systems diff --git a/docs/docs/writing-tests/hooks-cleanup.md b/docs/docs/writing-tests/hooks-cleanup.md index b07d96190d..a42fba2e71 100644 --- a/docs/docs/writing-tests/hooks-cleanup.md +++ b/docs/docs/writing-tests/hooks-cleanup.md @@ -31,7 +31,7 @@ public async Task AsyncCleanup() // ✅ Valid - asynchronous hook ### Hook Parameters :::info -`[After]` hooks accept the same parameters as `[Before]` hooks. See [Setup Hooks -- Hook Parameters](setup.md#hook-parameters) for the full reference. +`[After]` hooks accept the same parameters as `[Before]` hooks. See [Setup Hooks — Hook Parameters](hooks-setup.md#hook-parameters) for the full reference. ::: A common pattern in cleanup hooks is checking the test result to perform conditional cleanup: diff --git a/docs/docs/writing-tests/lifecycle.md b/docs/docs/writing-tests/lifecycle.md index dd5d09bf43..b78c6eee29 100644 --- a/docs/docs/writing-tests/lifecycle.md +++ b/docs/docs/writing-tests/lifecycle.md @@ -431,8 +431,8 @@ All `[After]` hooks, `ITestEndEventReceiver` events, and disposal methods run ev ## Related Pages -- [Test Set Ups](setup.md) - Detailed guide to `[Before]` hooks -- [Test Clean Ups](cleanup.md) - Detailed guide to `[After]` hooks +- [Test Set Ups](hooks-setup.md) - Detailed guide to `[Before]` hooks +- [Test Clean Ups](hooks-cleanup.md) - Detailed guide to `[After]` hooks - [Event Subscribing](event-subscribing.md) - Event receiver interfaces - [Property Injection](property-injection.md) - Property injection and `IAsyncInitializer` - [Dependency Injection](dependency-injection.md) - DI integration diff --git a/docs/src/components/ChooseYourJourney/index.tsx b/docs/src/components/ChooseYourJourney/index.tsx index 4de136a55c..7873c8115f 100644 --- a/docs/src/components/ChooseYourJourney/index.tsx +++ b/docs/src/components/ChooseYourJourney/index.tsx @@ -23,7 +23,7 @@ const journeys: JourneyCard[] = [ { label: 'Installation', href: '/docs/getting-started/installation' }, { label: 'Write Your First Test', href: '/docs/getting-started/writing-your-first-test' }, { label: 'Philosophy', href: '/docs/guides/philosophy' }, - { label: 'Core Concepts', href: '/docs/test-authoring/things-to-know' }, + { label: 'Core Concepts', href: '/docs/writing-tests/things-to-know' }, ], }, { @@ -46,8 +46,8 @@ const journeys: JourneyCard[] = [ links: [ { label: 'Assertions Library', href: '/docs/assertions/library' }, { label: 'Assertion Basics', href: '/docs/assertions/getting-started' }, - { label: 'Data Driven Testing', href: '/docs/test-authoring/arguments' }, - { label: 'Test Lifecycle', href: '/docs/test-lifecycle/setup' }, + { label: 'Data Driven Testing', href: '/docs/writing-tests/arguments' }, + { label: 'Test Lifecycle', href: '/docs/writing-tests/hooks-setup' }, ], }, { @@ -58,8 +58,8 @@ const journeys: JourneyCard[] = [ links: [ { label: 'Best Practices', href: '/docs/guides/best-practices' }, { label: 'Performance Benchmarks', href: '/docs/benchmarks' }, - { label: 'Parallel Execution', href: '/docs/parallelism/not-in-parallel' }, - { label: 'Customization', href: '/docs/customization-extensibility/data-source-generators' }, + { label: 'Parallel Execution', href: '/docs/execution/not-in-parallel' }, + { label: 'Customization', href: '/docs/extending/data-source-generators' }, ], }, ]; From 5a993998ba071365bead0bc577064d566b251d2d Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:54:24 +0000 Subject: [PATCH 7/9] =?UTF-8?q?docs:=20fix=20review=20findings=20=E2=80=94?= =?UTF-8?q?=20orphaned=20pages,=20typos,=20tone,=20missing=20await?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add delegates, regex-assertions, type-checking to sidebar (were orphaned) - Move F# assertions under Assertions section, troubleshooting to top-level - Rename "Test Set Ups" / "Test Clean Ups" to "Setup Hooks" / "Cleanup Hooks" - Fix "defined it finishes" typos in hooks-cleanup.md - Fix dash-capital pattern across hooks and ordering pages - Fix # → ## heading level for IDE Support in running-your-tests.md - Fix Test Lifecycle link pointing to hooks-setup instead of lifecycle - Add missing await on assertions in methodology, generic-attributes - Use short-form [Before(Test)] in libraries.md example - Remove potentially fabricated testconfig.json coverage config - Fix casual contractions and first-person voice - Clean up marketing tone in framework-differences.md - Add cross-references between setup and cleanup hook pages --- docs/docs/assertions/type-checking.md | 4 +-- docs/docs/benchmarks/methodology.md | 4 +-- docs/docs/comparison/framework-differences.md | 19 +++++++------- docs/docs/extending/code-coverage.md | 25 ++----------------- docs/docs/extending/libraries.md | 5 ++-- docs/docs/getting-started/installation.md | 6 ++--- .../getting-started/running-your-tests.md | 6 ++--- docs/docs/writing-tests/generic-attributes.md | 12 ++++----- docs/docs/writing-tests/hooks-cleanup.md | 8 +++--- docs/docs/writing-tests/hooks-setup.md | 9 +++++-- docs/docs/writing-tests/ordering.md | 2 +- docs/sidebars.ts | 7 ++++-- 12 files changed, 46 insertions(+), 61 deletions(-) diff --git a/docs/docs/assertions/type-checking.md b/docs/docs/assertions/type-checking.md index cba961bdd4..76488b2dbf 100644 --- a/docs/docs/assertions/type-checking.md +++ b/docs/docs/assertions/type-checking.md @@ -4,9 +4,7 @@ sidebar_position: 5 # Type Checking -TUnit assertions try to check the types at compile time. -This gives faster developer feedback and helps speed up development time. -(Ever made a silly mistake on a test, but haven't realised till 15 minutes later after your build pipeline has finally told you? I know I have!) +TUnit assertions check types at compile time wherever possible. This gives faster feedback and catches mistakes before your build pipeline runs. So this wouldn't compile, because we're comparing an `int` and a `string`: diff --git a/docs/docs/benchmarks/methodology.md b/docs/docs/benchmarks/methodology.md index b9fa06d5b1..7a77d1efb8 100644 --- a/docs/docs/benchmarks/methodology.md +++ b/docs/docs/benchmarks/methodology.md @@ -44,9 +44,9 @@ All benchmarks use [BenchmarkDotNet](https://benchmarkdotnet.org/), the industry [Arguments(1, 2, 3)] [Arguments(4, 5, 9)] // ... 50 argument sets -public void TestAddition(int a, int b, int expected) +public async Task TestAddition(int a, int b, int expected) { - Assert.That(a + b).IsEqualTo(expected); + await Assert.That(a + b).IsEqualTo(expected); } ``` diff --git a/docs/docs/comparison/framework-differences.md b/docs/docs/comparison/framework-differences.md index 1c113adc6f..5d8ba31fc2 100644 --- a/docs/docs/comparison/framework-differences.md +++ b/docs/docs/comparison/framework-differences.md @@ -115,11 +115,11 @@ NUnit assertions largely influenced the way that TUnit assertions work. However, ## Other -### Source generated + Native AOT Support + Single File Support -As mentioned, TUnit is source generated. This should mean things are fast. And you can check out the generated code yourself! Because tests are source generated and not scanned via reflection, this means you can build your test projects using Native AOT or as a Single File application - Something that you can't current do with NUnit or xUnit. +### Source Generated + Native AOT + Single File Support +TUnit is source generated, so test discovery happens at compile time rather than through runtime reflection. You can inspect the generated code yourself. Because tests are source generated, you can build your test projects using Native AOT or as a Single File application — something that NUnit and xUnit do not currently support. -### More lifecycle hooks -TUnit has tried to make it easy to hook into a range of lifecycles. +### More Lifecycle Hooks +TUnit provides a wide range of lifecycle hook points. The attributes you can use on your hook methods are: - `[Before(Test)]` - Run before every test in the class it's defined in - `[After(Test)]` - Run after every test in the class it's defined in @@ -139,13 +139,12 @@ The attributes you can use on your hook methods are: - `[BeforeEvery(Assembly)]` - Run before the first test in every assembly in the test run - `[AfterEvery(Assembly)]` - Run after the last test in every assembly in the test run -And all those hooks allow injecting in a relevant `[HookType]Context` object - So you can interrogate it for information about the test run so far. Hopefully meeting the needs of most users! +All hooks accept a relevant `[HookType]Context` object, giving you access to information about the current test run. -### Test dependencies -Got tests that require another test to execute first? -In other frameworks it usually involves turning off parallelisation, then setting an `[Order]` attribute with 1, 2, 3, etc. -In TUnit, you can use a `[DependsOn(...)]` attribute. That test will wait to start, only once its dependencies have finished. And you don't have to turn off parallelisation of other tests! +### Test Dependencies +In other frameworks, running tests in a specific order usually requires turning off parallelisation and setting an `[Order]` attribute with 1, 2, 3, etc. +In TUnit, you can use a `[DependsOn(...)]` attribute. That test will wait to start until its dependencies have finished, without disabling parallelisation for other tests. ```csharp [Test] @@ -170,4 +169,4 @@ In TUnit, you can use a `[DependsOn(...)]` attribute. That test will wait to sta ``` ### Class Arguments -A lot of the data injection mechanisms in xUnit/NUnit work for the method, or the class, and not vice-versa. With TUnit, you can use `[Arguments(...)]` or `[Matrix(...)]` or `[MethodDataSource(...)]` etc. for both classes and test methods, making it super flexible! +Many data injection mechanisms in xUnit/NUnit work for either the method or the class, but not both. With TUnit, you can use `[Arguments(...)]`, `[Matrix(...)]`, `[MethodDataSource(...)]`, and other data attributes on both classes and test methods. diff --git a/docs/docs/extending/code-coverage.md b/docs/docs/extending/code-coverage.md index 63f7af0306..a83f1a39bc 100644 --- a/docs/docs/extending/code-coverage.md +++ b/docs/docs/extending/code-coverage.md @@ -69,30 +69,9 @@ Tools for viewing results: ## Configuration -### testconfig.json - -You can customize coverage behavior with a `testconfig.json` file placed in the same directory as your test project: - -```json -{ - "codeCoverage": { - "Configuration": { - "CodeCoverage": { - "ModulePaths": { - "Include": [".*\\.dll$"], - "Exclude": [".*tests\\.dll$"] - } - } - } - } -} -``` - -The file is picked up automatically when running tests. - -### XML Settings File +### Coverage Settings File -Alternatively, you can use an XML coverage settings file: +You can customize coverage behavior (include/exclude modules, etc.) with a settings file: ```bash dotnet run --configuration Release --coverage --coverage-settings coverage.config diff --git a/docs/docs/extending/libraries.md b/docs/docs/extending/libraries.md index ec97905b6e..13519cd53f 100644 --- a/docs/docs/extending/libraries.md +++ b/docs/docs/extending/libraries.md @@ -31,18 +31,19 @@ This produces a class library (`.dll`), not an executable. ```csharp using TUnit.Core; +using static TUnit.Core.HookType; namespace MyCompany.Testing; public abstract class DatabaseTestBase { - [Before(HookType.Test)] + [Before(Test)] public async Task ResetDatabase() { await TestDatabase.ResetAsync(); } - [After(HookType.Test)] + [After(Test)] public async Task CleanupConnections() { await TestDatabase.CloseConnectionsAsync(); diff --git a/docs/docs/getting-started/installation.md b/docs/docs/getting-started/installation.md index 613e962eb0..7c7589a38d 100644 --- a/docs/docs/getting-started/installation.md +++ b/docs/docs/getting-started/installation.md @@ -1,8 +1,8 @@ # Installing TUnit -## Easily +## Quick Start -Assuming you have the .NET SDK installed, simply run: +Assuming you have the .NET SDK installed, run: `dotnet new install TUnit.Templates` @@ -25,7 +25,7 @@ cd YourTestProjectNameHere dotnet add package TUnit --prerelease ``` -And then remove any automatically generated `Program.cs` or main method, as this'll be taken care of by the TUnit package. +And then remove any automatically generated `Program.cs` or main method, as this is handled by the TUnit package. ### Global Usings diff --git a/docs/docs/getting-started/running-your-tests.md b/docs/docs/getting-started/running-your-tests.md index ce9a202225..23b83928b0 100644 --- a/docs/docs/getting-started/running-your-tests.md +++ b/docs/docs/getting-started/running-your-tests.md @@ -53,7 +53,7 @@ dotnet YourTestProject.dll --report-trx --coverage ## Published Test Project When you publish your test project, you'll be given an executable. -On windows this'll be a `.exe` and on Linux/MacOS there'll be no extension. +On Windows this will be a `.exe` and on Linux/macOS there will be no extension. This can be invoked directly and passed any flags. @@ -64,7 +64,7 @@ cd 'C:/Your/Test/Directory/bin/Release/net8.0/win-x64/publish' ./YourTestProject.exe --report-trx --coverage ``` -# IDE Support +## IDE Support ## Visual Studio Visual Studio is supported. The "Use testing platform server mode" option must be selected in Tools > Manage Preview Features. @@ -98,7 +98,7 @@ To continue your journey with TUnit, explore these topics: **Core Testing Concepts:** - **[Assertions](../assertions/getting-started.md)** - Learn TUnit's fluent assertion syntax -- **[Test Lifecycle](../writing-tests/hooks-setup.md)** - Set up and tear down test state with hooks +- **[Test Lifecycle](../writing-tests/lifecycle.md)** - Understand the test execution lifecycle - **[Data-Driven Testing](../writing-tests/arguments.md)** - Run tests with multiple input values **Common Tasks:** diff --git a/docs/docs/writing-tests/generic-attributes.md b/docs/docs/writing-tests/generic-attributes.md index 6383ed63fd..1ef11b5a61 100644 --- a/docs/docs/writing-tests/generic-attributes.md +++ b/docs/docs/writing-tests/generic-attributes.md @@ -23,10 +23,10 @@ public class CalculatorTests { [Test] [MethodDataSource(nameof(TestDataProviders.AdditionTestCases))] - public void Add_ShouldReturnCorrectSum(int a, int b, int expected) + public async Task Add_ShouldReturnCorrectSum(int a, int b, int expected) { var result = Calculator.Add(a, b); - Assert.That(result).IsEqualTo(expected); + await Assert.That(result).IsEqualTo(expected); } } ``` @@ -135,9 +135,9 @@ public class RandomNumbersAttribute : DataSourceGeneratorAttribute // Usage [Test] [RandomNumbers(5, min: 1, max: 10)] -public void TestWithRandomNumbers(int number) +public async Task TestWithRandomNumbers(int number) { - Assert.That(number).IsBetween(1, 10); + await Assert.That(number).IsBetween(1, 10); } ``` @@ -238,11 +238,11 @@ public class ScenarioDataSource : TypedDataSourceAttribute [Test] [ScenarioDataSource] -public void TestCalculation(CalculationScenario scenario) +public async Task TestCalculation(CalculationScenario scenario) { var (a, b) = scenario.Input; var result = Calculator.Add(a, b); - Assert.That(result).IsEqualTo(scenario.Expected); + await Assert.That(result).IsEqualTo(scenario.Expected); } ``` diff --git a/docs/docs/writing-tests/hooks-cleanup.md b/docs/docs/writing-tests/hooks-cleanup.md index a42fba2e71..0a10553ecf 100644 --- a/docs/docs/writing-tests/hooks-cleanup.md +++ b/docs/docs/writing-tests/hooks-cleanup.md @@ -1,4 +1,4 @@ -# Test Clean Ups +# Cleanup Hooks TUnit supports having your test class implement `IDisposable` or `IAsyncDisposable`. These will be called after your test has finished executing. However, using the attributes below offers better support for running multiple methods, and without having to implement your own try/catch logic. Every `[After]` method will be run, and any exceptions will be lazily thrown afterwards. @@ -55,10 +55,10 @@ Must be an instance method. Will be executed after each test in the class it's d Methods will be executed top-down, so the current class clean ups will execute first, then the base classes' last. ### [After(Class)] -Must be a static method. Will run once after the last test in the class it's defined it finishes. +Must be a static method. Will run once after the last test in the class it's defined in finishes. ### [After(Assembly)] -Must be a static method. Will run once after the last test in the assembly it's defined it finishes. +Must be a static method. Will run once after the last test in the assembly it's defined in finishes. ### [After(TestSession)] Must be a static method. Will run once after the last test in the test session finishes. @@ -67,7 +67,7 @@ Must be a static method. Will run once after the last test in the test session f Must be a static method. Will run once after tests are discovered. ## [AfterEvery(HookType)] -All [AfterEvery(...)] methods must be static - And should ideally be placed in their own file that's easy to find, as they can globally affect the test suite, so it should be easy for developers to locate this behaviour. +All [AfterEvery(...)] methods must be static. They should ideally be placed in their own file that's easy to find, as they can globally affect the test suite, so it should be easy for developers to locate this behaviour. e.g. `GlobalHooks.cs` at the root of the test project. :::info diff --git a/docs/docs/writing-tests/hooks-setup.md b/docs/docs/writing-tests/hooks-setup.md index a66033d2c7..90c238e3ec 100644 --- a/docs/docs/writing-tests/hooks-setup.md +++ b/docs/docs/writing-tests/hooks-setup.md @@ -1,4 +1,4 @@ -# Test Set Ups +# Setup Hooks Most setup for a test can be performed in the constructor (think setting up mocks, assigning fields.) @@ -103,7 +103,7 @@ Must be a static method. Will run once before the first test in the test session Must be a static method. Will run once before any tests are discovered. ## [BeforeEvery(HookType)] -All [BeforeEvery(...)] methods must be static - And should ideally be placed in their own file that's easy to find, as they can globally affect the test suite, so it should be easy for developers to locate this behaviour. +All [BeforeEvery(...)] methods must be static. They should ideally be placed in their own file that's easy to find, as they can globally affect the test suite, so it should be easy for developers to locate this behaviour. e.g. `GlobalHooks.cs` at the root of the test project. ### [BeforeEvery(Test)] @@ -339,3 +339,8 @@ E.g. context.AddAsyncLocalValues(); } ``` + +## See Also + +- [Cleanup Hooks](hooks-cleanup.md) — Teardown logic with `[After]` and `[AfterEvery]` +- [Test Lifecycle](lifecycle.md) — Full overview of the test execution lifecycle diff --git a/docs/docs/writing-tests/ordering.md b/docs/docs/writing-tests/ordering.md index 48f68b1ec0..b09d877806 100644 --- a/docs/docs/writing-tests/ordering.md +++ b/docs/docs/writing-tests/ordering.md @@ -134,7 +134,7 @@ If your tests depends on another test, it's possible to retrieve that test's con This is done by calling the `GetTests` method on the `TestContext.Dependencies` property. It takes the test's method name (so you can use `nameof(...)`) and optionally the parameter types for if there's multiple overloads. -You'll notice this returns an array - This is because tests may be data driven and be invoked multiple times - If this is the case you'll have to find the one you want yourself. +You'll notice this returns an array. This is because tests may be data driven and be invoked multiple times. If this is the case, you'll have to find the one you want yourself. :::info For data-driven tests (e.g., with `[Arguments]` or `[MethodDataSource]`), `GetTests` returns all invocations. diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 6546d1acd6..e124d26e61 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -116,10 +116,14 @@ const sidebars: SidebarsConfig = { 'assertions/awaiting', 'assertions/tasks-and-async', 'assertions/exceptions', + 'assertions/delegates', ], }, 'assertions/combining-assertions', 'assertions/member-assertions', + 'assertions/regex-assertions', + 'assertions/type-checking', + 'assertions/fsharp', { type: 'category', label: 'Custom Assertions', @@ -205,10 +209,9 @@ const sidebars: SidebarsConfig = { 'examples/fsharp-interactive', 'examples/tunit-ci-pipeline', 'examples/instrumenting-global-test-ids', - 'assertions/fsharp', - 'troubleshooting', ], }, + 'troubleshooting', { type: 'category', label: 'Reference', From 10b3445348583e39b022804d5f9e6af9b56b17d2 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:10:56 +0000 Subject: [PATCH 8/9] =?UTF-8?q?docs:=20polish=20and=20strengthen=20?= =?UTF-8?q?=E2=80=94=20fix=20bugs,=20expand=20thin=20pages,=20merge=20para?= =?UTF-8?q?llelism?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix homepage code examples (un-awaited assertions, void→async Task) - Fix ChooseYourJourney link targets (migration overview, test lifecycle) - Rewrite misleading extensions info box in running-your-tests - Change navbar/footer label from "Tutorial" to "Docs" - Remove unrendered JS date expression in methodology.md - Add prose to assertions/library.md - Expand type-checking.md with IsTypeOf, IsAssignableTo, negative cases - Expand culture.md with "Why This Matters" section and more examples - Add doc section overview to intro.md - Trim C# operator testing from null-and-default.md - Add assertion bodies to empty test methods in parallelism examples - Merge not-in-parallel, parallel-groups, parallel-limiter into single execution/parallelism.md with comparison table - Reorder sidebar: move awaiting before Value Assertions, library to end - Add Next links to getting-started pages and See Also sections - Move auto-import note to top of writing-your-first-test step-by-step - Fix over-indented code blocks in filebased-csharp.md - Replace placeholder connection string in aspnet.md - Remove 10 orphaned Docusaurus default images --- docs/docs/assertions/combining-assertions.md | 5 + docs/docs/assertions/library.md | 2 + docs/docs/assertions/null-and-default.md | 42 ---- docs/docs/assertions/type-checking.md | 88 ++++++++- docs/docs/benchmarks/methodology.md | 2 - docs/docs/examples/aspnet.md | 2 +- docs/docs/examples/filebased-csharp.md | 48 +++-- docs/docs/execution/not-in-parallel.md | 59 ------ docs/docs/execution/parallel-groups.md | 80 -------- docs/docs/execution/parallel-limiter.md | 80 -------- docs/docs/execution/parallelism.md | 182 ++++++++++++++++++ docs/docs/getting-started/installation.md | 2 + .../getting-started/running-your-tests.md | 4 +- .../writing-your-first-test.md | 30 +-- docs/docs/intro.md | 11 ++ docs/docs/writing-tests/culture.md | 71 ++++++- docs/docs/writing-tests/things-to-know.md | 6 + docs/docusaurus.config.ts | 4 +- docs/sidebars.ts | 15 +- .../components/ChooseYourJourney/index.tsx | 6 +- .../src/components/HomepageFeatures/index.tsx | 8 +- docs/static/img/docusaurus.png | Bin 5142 -> 0 bytes docs/static/img/easy.svg | 1 - docs/static/img/fast.svg | 1 - docs/static/img/favicon.ico | Bin 3626 -> 0 bytes docs/static/img/flexible.svg | 1 - docs/static/img/lab.svg | 1 - docs/static/img/logo.svg | 1 - .../static/img/undraw_docusaurus_mountain.svg | 171 ---------------- docs/static/img/undraw_docusaurus_react.svg | 170 ---------------- docs/static/img/undraw_docusaurus_tree.svg | 40 ---- 31 files changed, 395 insertions(+), 738 deletions(-) delete mode 100644 docs/docs/execution/not-in-parallel.md delete mode 100644 docs/docs/execution/parallel-groups.md delete mode 100644 docs/docs/execution/parallel-limiter.md create mode 100644 docs/docs/execution/parallelism.md delete mode 100644 docs/static/img/docusaurus.png delete mode 100644 docs/static/img/easy.svg delete mode 100644 docs/static/img/fast.svg delete mode 100644 docs/static/img/favicon.ico delete mode 100644 docs/static/img/flexible.svg delete mode 100644 docs/static/img/lab.svg delete mode 100644 docs/static/img/logo.svg delete mode 100644 docs/static/img/undraw_docusaurus_mountain.svg delete mode 100644 docs/static/img/undraw_docusaurus_react.svg delete mode 100644 docs/static/img/undraw_docusaurus_tree.svg diff --git a/docs/docs/assertions/combining-assertions.md b/docs/docs/assertions/combining-assertions.md index 766daad168..7ee43d2e33 100644 --- a/docs/docs/assertions/combining-assertions.md +++ b/docs/docs/assertions/combining-assertions.md @@ -78,3 +78,8 @@ public async Task MyTest() ``` Both forms aggregate failures. When the `using` scope ends, any accumulated assertion failures are thrown as a single exception listing all violations. + +## See Also + +- [Getting Started with Assertions](getting-started.md) — Assertion basics and fluent syntax +- [Exception Assertions](exceptions.md) — Assert on thrown exceptions diff --git a/docs/docs/assertions/library.md b/docs/docs/assertions/library.md index 85a59de9a9..6e1fc85088 100644 --- a/docs/docs/assertions/library.md +++ b/docs/docs/assertions/library.md @@ -6,4 +6,6 @@ sidebar_position: 100 import AssertionsLibrary from '@site/src/components/AssertionsLibrary'; +Browse the full list of assertions available in TUnit. Use the search box to filter by name or category. Click any assertion to see its signature and usage details. + diff --git a/docs/docs/assertions/null-and-default.md b/docs/docs/assertions/null-and-default.md index e681059388..e126e3aba4 100644 --- a/docs/docs/assertions/null-and-default.md +++ b/docs/docs/assertions/null-and-default.md @@ -401,48 +401,6 @@ public async Task DateTime_Default() } ``` -## Combining with Other Assertions - -### Null Coalescing Validation - -```csharp -[Test] -public async Task Null_Coalescing_Default() -{ - string? input = GetOptionalInput(); - string result = input ?? "default"; - - if (input == null) - { - await Assert.That(result).IsEqualTo("default"); - } - else - { - await Assert.That(result).IsEqualTo(input); - } -} -``` - -### Null Conditional Operator - -```csharp -[Test] -public async Task Null_Conditional() -{ - Person? person = FindPerson("id"); - string? name = person?.Name; - - if (person == null) - { - await Assert.That(name).IsNull(); - } - else - { - await Assert.That(name).IsNotNull(); - } -} -``` - ## Common Patterns ### Validate Required Dependencies diff --git a/docs/docs/assertions/type-checking.md b/docs/docs/assertions/type-checking.md index 76488b2dbf..d4619674ed 100644 --- a/docs/docs/assertions/type-checking.md +++ b/docs/docs/assertions/type-checking.md @@ -6,12 +6,86 @@ sidebar_position: 5 TUnit assertions check types at compile time wherever possible. This gives faster feedback and catches mistakes before your build pipeline runs. -So this wouldn't compile, because we're comparing an `int` and a `string`: +For example, this wouldn't compile because we're comparing an `int` and a `string`: ```csharp - [Test] - public async Task MyTest() - { - await Assert.That(1).IsEqualTo("1"); - } -``` \ No newline at end of file +[Test] +public async Task MyTest() +{ + await Assert.That(1).IsEqualTo("1"); +} +``` + +## Runtime Type Assertions + +When you need to verify types at runtime — for example, when working with polymorphic return types — TUnit provides dedicated assertions. + +### IsTypeOf + +Tests that a value is exactly the specified type (not a subclass): + +```csharp +[Test] +public async Task Exact_Type() +{ + object result = GetAnimal(); + + await Assert.That(result).IsTypeOf(); +} +``` + +### IsAssignableTo + +Tests that a value can be assigned to the specified type, including base classes and interfaces: + +```csharp +[Test] +public async Task Assignable_To_Base_Or_Interface() +{ + object result = GetAnimal(); + + await Assert.That(result).IsAssignableTo(); + await Assert.That(result).IsAssignableTo(); +} +``` + +### IsNotTypeOf + +Tests that a value is **not** exactly the specified type: + +```csharp +[Test] +public async Task Not_Exact_Type() +{ + Animal animal = GetAnimal(); + + await Assert.That(animal).IsNotTypeOf(); +} +``` + +### IsNotAssignableTo + +Tests that a value cannot be assigned to the specified type: + +```csharp +[Test] +public async Task Not_Assignable() +{ + object result = GetAnimal(); + + await Assert.That(result).IsNotAssignableTo(); +} +``` + +## Delegate Return Types + +Type assertions also work on delegate return values, letting you verify the type returned by a method or lambda: + +```csharp +[Test] +public async Task Delegate_Return_Type() +{ + await Assert.That(() => GetAnimal()).IsTypeOf(); + await Assert.That(async () => await GetAnimalAsync()).IsAssignableTo(); +} +``` diff --git a/docs/docs/benchmarks/methodology.md b/docs/docs/benchmarks/methodology.md index 7a77d1efb8..bd3217bd03 100644 --- a/docs/docs/benchmarks/methodology.md +++ b/docs/docs/benchmarks/methodology.md @@ -286,5 +286,3 @@ Found an issue with the benchmarks? [Open an issue](https://github.com/thomhurst - [BenchmarkDotNet Documentation](https://benchmarkdotnet.org/articles/overview.html) - [.NET Performance Best Practices](https://learn.microsoft.com/en-us/dotnet/framework/performance/) - [TUnit Performance Best Practices](/docs/guides/performance) - -*Last updated: {new Date().toISOString().split('T')[0]}* diff --git a/docs/docs/examples/aspnet.md b/docs/docs/examples/aspnet.md index 01ac899279..6a37760caa 100644 --- a/docs/docs/examples/aspnet.md +++ b/docs/docs/examples/aspnet.md @@ -27,7 +27,7 @@ public class WebApplicationFactory : TestWebApplicationFactory { config.AddInMemoryCollection(new Dictionary { - { "ConnectionStrings:Default", "..." } + { "ConnectionStrings:Default", "Server=localhost;Database=TestDb" } }); }); } diff --git a/docs/docs/examples/filebased-csharp.md b/docs/docs/examples/filebased-csharp.md index 746b418be0..4916be4a38 100644 --- a/docs/docs/examples/filebased-csharp.md +++ b/docs/docs/examples/filebased-csharp.md @@ -31,34 +31,32 @@ To use TUnit with a file-based C# application, you can follow these steps: 3. **Write your tests**: You can write your tests in the same way you would in a regular C# project. For example: - ```csharp - #:package TUnit@0.* + ```csharp + #:package TUnit@0.* - using TUnit; - public class Tests + using TUnit; + public class Tests + { + [Test] + public void Basic() { - [Test] - public void Basic() - { - Console.WriteLine("This is a basic test"); - } - - [Test] - [Arguments(1, 2, 3)] - [Arguments(2, 3, 5)] - public async Task DataDrivenArguments(int a, int b, int c) - { - Console.WriteLine("This one can accept arguments from an attribute"); - var result = a + b; - await Assert.That(result).IsEqualTo(c); - } - + Console.WriteLine("This is a basic test"); } - ``` + [Test] + [Arguments(1, 2, 3)] + [Arguments(2, 3, 5)] + public async Task DataDrivenArguments(int a, int b, int c) + { + Console.WriteLine("This one can accept arguments from an attribute"); + var result = a + b; + await Assert.That(result).IsEqualTo(c); + } + } + ``` 4. **Run your tests**: You can run your tests by executing the script using `dotnet run`. The results will be printed to the console. - To run the script, you can use the following command + To run the script, you can use the following command: ```powershell dotnet run Program.cs @@ -66,9 +64,9 @@ To use TUnit with a file-based C# application, you can follow these steps: If you need to convert the file based application to a regular C# project, you can run the following command: - ```powershell - dotnet project convert Program.cs - ``` +```powershell +dotnet project convert Program.cs +``` ## Using msbuild props with File-Based C# Application diff --git a/docs/docs/execution/not-in-parallel.md b/docs/docs/execution/not-in-parallel.md deleted file mode 100644 index 4c82ea9a64..0000000000 --- a/docs/docs/execution/not-in-parallel.md +++ /dev/null @@ -1,59 +0,0 @@ -# Not in Parallel - -By default, TUnit tests will run in parallel. - -:::performance -Parallel execution is a major contributor to TUnit's speed advantage. Running tests in parallel can dramatically reduce total test suite execution time. See the [performance benchmarks](/docs/benchmarks) for real-world performance data. -::: - -To remove this behaviour, we can add a `[NotInParallel]` attribute to our test methods or classes. - -This also takes an optional array of constraint keys. - -If no constraint keys are supplied, then the test will only be run by itself. -If any constraint keys are set, the test will not be run alongside any other tests with any of those same keys. However it may still run in parallel alongside tests with other constraint keys. - -For the example below, `MyTest` and `MyTest2` will not run in parallel with each other because of the common `DatabaseTest` constraint key, but `MyTest3` may run in parallel with the other two. - -```csharp -using TUnit.Core; - -namespace MyTestProject; - -public class MyTestClass -{ - private const string DatabaseTest = "DatabaseTest"; - private const string RegistrationTest = "RegistrationTest"; - private const string ParallelTest = "ParallelTest"; - - [Test] - [NotInParallel(DatabaseTest)] - public async Task MyTest() - { - - } - - [Test] - [NotInParallel(new[] { DatabaseTest, RegistrationTest })] - public async Task MyTest2() - { - - } - - [Test] - [NotInParallel(ParallelTest)] - public async Task MyTest3() - { - - } -} -``` - -## Global [NotInParallel] - -If you want to disable parallelism for all tests in an assembly (To run tests sequentially), -you can add the following: - -```csharp -[assembly: NotInParallel] -``` diff --git a/docs/docs/execution/parallel-groups.md b/docs/docs/execution/parallel-groups.md deleted file mode 100644 index 8cbf8ca161..0000000000 --- a/docs/docs/execution/parallel-groups.md +++ /dev/null @@ -1,80 +0,0 @@ -# Parallel Groups - -Parallel groups control which tests can run at the same time. Classes that share the same `[ParallelGroup("key")]` are batched together: tests within the same group run in parallel with each other, but no tests from other groups run alongside them. The engine finishes one group entirely before starting the next. - -## How It Works - -Consider two groups: - -- **Group A**: `ClassA1` and `ClassA2` both have `[ParallelGroup("GroupA")]` -- **Group B**: `ClassB1` has `[ParallelGroup("GroupB")]` - -At runtime: -1. All tests from `ClassA1` and `ClassA2` run in parallel with each other. No other tests execute during this phase. -2. Once every test in Group A finishes, all tests from `ClassB1` run. No other tests execute during this phase. - -Tests that do not belong to any parallel group run separately, following the normal parallel execution rules. - -## Example - -```csharp -using TUnit.Core; - -namespace MyTestProject; - -[ParallelGroup("Database")] -public class UserRepositoryTests -{ - [Test] - public async Task Create_User() - { - var user = await UserRepository.CreateAsync("alice"); - - await Assert.That(user.Name).IsEqualTo("alice"); - } - - [Test] - public async Task Delete_User() - { - await UserRepository.DeleteAsync("bob"); - - var exists = await UserRepository.ExistsAsync("bob"); - - await Assert.That(exists).IsFalse(); - } -} - -[ParallelGroup("Database")] -public class OrderRepositoryTests -{ - [Test] - public async Task Create_Order() - { - var order = await OrderRepository.CreateAsync("item-1"); - - await Assert.That(order.Id).IsNotNull(); - } -} - -[ParallelGroup("ExternalApi")] -public class PaymentApiTests -{ - [Test] - public async Task Charge_Succeeds() - { - var result = await PaymentApi.ChargeAsync(100); - - await Assert.That(result.Success).IsTrue(); - } -} -``` - -`UserRepositoryTests` and `OrderRepositoryTests` share `"Database"`, so their tests all run in parallel with each other. `PaymentApiTests` is in `"ExternalApi"`, so it runs in a separate phase -- after the `"Database"` group finishes (or before, depending on scheduling order). - -## Difference from `[NotInParallel]` - -`[NotInParallel]` prevents individual tests from running at the same time as other tests that share the same constraint key. Each test with `[NotInParallel("Database")]` runs one at a time, sequentially. - -`[ParallelGroup("Database")]` allows tests within the group to run in parallel with each other. Only tests from *other* groups are excluded during that phase. - -Use `[NotInParallel]` when tests must never overlap. Use `[ParallelGroup]` when tests can safely overlap with each other but must be isolated from unrelated tests. diff --git a/docs/docs/execution/parallel-limiter.md b/docs/docs/execution/parallel-limiter.md deleted file mode 100644 index 780cfba505..0000000000 --- a/docs/docs/execution/parallel-limiter.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -sidebar_position: 11 ---- - -# Parallel Limiter - -TUnit allows the user to control the parallel limit on a test, class or assembly level. - -To do this, we add a `[ParallelLimiter<>]` attribute. - -You'll notice this has a generic type argument - You must give it a type that implements `IParallelLimit` and has a public empty constructor. That interface requires you to define what the limit is for those tests. - -If a class doesn't have a parallel limit defined, it'll try and eagerly run when the .NET thread pool allows it to do so, so the upper limit is unknown. - -If it does have a parallel limit defined, be aware that that parallel limit is shared for any tests with that same `Type` of parallel limit. - -In the example below, `MyParallelLimit` has a limit of `2`. Now any test, anywhere in your test suite, that has this parallel limit attribute applied to it, will shared this limit, and so only 2 can be processed at a time. - -Other tests without this attribute may run alongside them still. - -And other tests with a different `Type` of parallel limit may also run alongside them still, but limited amongst themselves by their shared `Type` and limit. - -So be aware that limits are only shared among tests with that same `IParallelLimit` `Type`. - -So if you wanted to do a global limit on an assembly, you could do: - -```csharp -[assembly: ParallelLimiter] -``` - -And as long as that isn't overridden on a test or class, then that will apply to all tests in an assembly and be shared among them all, limiting how many run in parallel. - -## Example - -```csharp -using TUnit.Core; - -namespace MyTestProject; - -[ParallelLimiter] -public class MyTestClass -{ - [Test, Repeat(10)] - public async Task MyTest() - { - - } - - [Test, Repeat(10)] - public async Task MyTest2() - { - - } -} - -public record MyParallelLimit : IParallelLimit -{ - public int Limit => 2; -} -``` - -## Caveats -If a test uses `[DependsOn(nameof(OtherTest))]` and the other test has its own different parallel limit, this isn't guaranteed to be honoured. - -## Global Parallel Limit - -In case you want to apply the Parallel Limit logic to all tests in a project, you can add the attribute on the assembly level. - -```csharp -[assembly: ParallelLimiter] -``` - -The more specific attribute will always override the more general one. -For example, the `[ParallelLimiter]` on a method will override the `[ParallelLimiter]` on the class, -which in turn will override the `[ParallelLimiter]` on the assembly. - -So the order of precedence is: -1. Method -1. Class -1. Assembly diff --git a/docs/docs/execution/parallelism.md b/docs/docs/execution/parallelism.md new file mode 100644 index 0000000000..42efb21b0b --- /dev/null +++ b/docs/docs/execution/parallelism.md @@ -0,0 +1,182 @@ +--- +sidebar_position: 10 +--- + +# Controlling Parallelism + +TUnit runs all tests in parallel by default. This page covers three attributes that give you fine-grained control when you need it. + +:::performance +Parallel execution is a major contributor to TUnit's speed advantage. See the [performance benchmarks](/docs/benchmarks) for real-world data. +::: + +## Default Behavior + +With no attributes, every test is eligible to run concurrently. The .NET thread pool determines how many execute at once. For most test suites this is the fastest option and requires no configuration. + +## `[NotInParallel]` — Disabling Parallelism + +Add `[NotInParallel]` to prevent a test from running at the same time as other constrained tests. + +It accepts an optional array of **constraint keys**. Tests that share any key will never overlap. Tests with no common key may still run concurrently. + +If no keys are supplied, the test runs completely alone — no other test executes at the same time. + +```csharp +using TUnit.Core; + +namespace MyTestProject; + +public class MyTestClass +{ + private const string DatabaseTest = "DatabaseTest"; + private const string RegistrationTest = "RegistrationTest"; + private const string ParallelTest = "ParallelTest"; + + [Test] + [NotInParallel(DatabaseTest)] + public async Task MyTest() + { + var count = await Database.GetUserCountAsync(); + await Assert.That(count).IsGreaterThanOrEqualTo(0); + } + + [Test] + [NotInParallel(new[] { DatabaseTest, RegistrationTest })] + public async Task MyTest2() + { + var user = await Database.CreateUserAsync("alice"); + await Assert.That(user.Name).IsEqualTo("alice"); + } + + [Test] + [NotInParallel(ParallelTest)] + public async Task MyTest3() + { + var result = await Api.PingAsync(); + await Assert.That(result.IsSuccess).IsTrue(); + } +} +``` + +`MyTest` and `MyTest2` share the `DatabaseTest` key, so they never overlap. `MyTest3` has a different key and may run alongside either of them. + +### Global `[NotInParallel]` + +Disable parallelism for every test in an assembly (run all tests sequentially): + +```csharp +[assembly: NotInParallel] +``` + +## `[ParallelGroup]` — Grouping Tests into Phases + +`[ParallelGroup("key")]` batches classes into groups. Tests within the same group run in parallel with each other, but no tests from other groups run at the same time. The engine finishes one group entirely before starting the next. + +```csharp +using TUnit.Core; + +namespace MyTestProject; + +[ParallelGroup("Database")] +public class UserRepositoryTests +{ + [Test] + public async Task Create_User() + { + var user = await UserRepository.CreateAsync("alice"); + await Assert.That(user.Name).IsEqualTo("alice"); + } + + [Test] + public async Task Delete_User() + { + await UserRepository.DeleteAsync("bob"); + var exists = await UserRepository.ExistsAsync("bob"); + await Assert.That(exists).IsFalse(); + } +} + +[ParallelGroup("Database")] +public class OrderRepositoryTests +{ + [Test] + public async Task Create_Order() + { + var order = await OrderRepository.CreateAsync("item-1"); + await Assert.That(order.Id).IsNotNull(); + } +} + +[ParallelGroup("ExternalApi")] +public class PaymentApiTests +{ + [Test] + public async Task Charge_Succeeds() + { + var result = await PaymentApi.ChargeAsync(100); + await Assert.That(result.Success).IsTrue(); + } +} +``` + +`UserRepositoryTests` and `OrderRepositoryTests` share the `"Database"` group, so all their tests run together. `PaymentApiTests` is in `"ExternalApi"` and runs in a separate phase. + +Tests not assigned to any group run separately under normal parallel execution rules. + +## `[ParallelLimiter]` — Limiting Concurrent Test Count + +`[ParallelLimiter]` caps how many tests sharing the same limiter type can run concurrently. The generic type argument must implement `IParallelLimit` with a public parameterless constructor. + +The limit is shared across **all** tests referencing the same `IParallelLimit` type. Tests with a different limiter type or no limiter are unaffected. + +```csharp +using TUnit.Core; + +namespace MyTestProject; + +[ParallelLimiter] +public class MyTestClass +{ + [Test, Repeat(10)] + public async Task MyTest() + { + await Assert.That(true).IsTrue(); + } + + [Test, Repeat(10)] + public async Task MyTest2() + { + await Assert.That(1 + 1).IsEqualTo(2); + } +} + +public record MyParallelLimit : IParallelLimit +{ + public int Limit => 2; +} +``` + +With a limit of `2`, at most two of these 20 test invocations execute at the same time. + +### Assembly-Level Limiter + +```csharp +[assembly: ParallelLimiter] +``` + +More specific attributes override less specific ones. Precedence: Method > Class > Assembly. + +## When to Use Which + +| Scenario | Attribute | +|---|---| +| Tests must never overlap (e.g., shared database state) | `[NotInParallel("key")]` | +| Groups of tests can overlap internally but must be isolated from other groups | `[ParallelGroup("key")]` | +| Tests can overlap but you need to cap concurrency (e.g., limited external connections) | `[ParallelLimiter]` | +| Disable all parallelism for an assembly | `[assembly: NotInParallel]` | + +## Caveats + +- If a test uses `[DependsOn(nameof(OtherTest))]` and the dependency has a different parallel limiter or group, ordering is not guaranteed. +- `[NotInParallel]` without keys is the most restrictive — the test runs completely alone. Use constraint keys when possible to allow unrelated tests to proceed. diff --git a/docs/docs/getting-started/installation.md b/docs/docs/getting-started/installation.md index 7c7589a38d..02c0333b7b 100644 --- a/docs/docs/getting-started/installation.md +++ b/docs/docs/getting-started/installation.md @@ -128,3 +128,5 @@ If you prefer to manage the Polyfill version yourself, you can: TUnit automatically sets `true` to ensure that Polyfill types are embedded in each project. This prevents type conflicts when using `InternalsVisibleTo` or when multiple projects in your solution reference Polyfill. Each project gets its own isolated copy of the polyfill types, following the [recommended Polyfill consuming pattern](https://github.com/SimonCropp/Polyfill/blob/main/consuming.md#recommended-consuming-pattern). You can override this behavior by setting `false` in your project file if needed. + +**Next:** [Write Your First Test →](writing-your-first-test.md) diff --git a/docs/docs/getting-started/running-your-tests.md b/docs/docs/getting-started/running-your-tests.md index 23b83928b0..4935181538 100644 --- a/docs/docs/getting-started/running-your-tests.md +++ b/docs/docs/getting-started/running-your-tests.md @@ -4,7 +4,7 @@ As TUnit is built on-top of the newer Microsoft.Testing.Platform, and combined w :::info -Please note that for the coverage and trx report, you need to install [additional extensions](../extending/built-in-extensions.md) +Coverage and TRX reporting are built in. See [Extensions](../extending/built-in-extensions.md) for usage flags. ::: @@ -107,7 +107,7 @@ To continue your journey with TUnit, explore these topics: - **[Cookbook](../guides/cookbook.md)** - Common testing patterns and recipes **Advanced Features:** -- **[Parallelism](../execution/not-in-parallel.md)** - Control how tests run in parallel +- **[Parallelism](../execution/parallelism.md)** - Control how tests run in parallel - **[CI/CD Integration](../execution/ci-cd-reporting.md)** - Integrate TUnit into your pipeline Need help? Check the [Troubleshooting & FAQ](../troubleshooting.md) guide. diff --git a/docs/docs/getting-started/writing-your-first-test.md b/docs/docs/getting-started/writing-your-first-test.md index 346ce02d08..e47d91ea97 100644 --- a/docs/docs/getting-started/writing-your-first-test.md +++ b/docs/docs/getting-started/writing-your-first-test.md @@ -32,6 +32,10 @@ public class CalculatorTests ## Step-by-Step Guide +:::tip Auto-Imported Namespaces +The TUnit package automatically configures global usings for `TUnit.Core`, `TUnit.Assertions`, and `TUnit.Assertions.Extensions`. The explicit `using` statements in the examples below are shown for clarity — you don't need them in practice. +::: + Start by creating a new class: ```csharp @@ -194,28 +198,4 @@ public class StringTests } ``` -### Using Statements - -The examples above show explicit using statements for clarity: - -```csharp -using TUnit.Core; // For [Test] attribute -using TUnit.Assertions; // For Assert.That() -using TUnit.Assertions.Extensions; // For assertion methods like IsEqualTo(), IsTrue(), etc. -``` - -**However**, the TUnit package automatically configures these namespaces as global usings, so in practice you don't need to include them in each test file. Your test classes can be as simple as: - -```csharp -namespace MyTestProject; - -public class ValidatorTests -{ - [Test] - public async Task IsPositive_WithNegativeNumber_ReturnsFalse() - { - var result = Validator.IsPositive(-1); - await Assert.That(result).IsFalse(); - } -} -``` +**Next:** [Run Your Tests →](running-your-tests.md) diff --git a/docs/docs/intro.md b/docs/docs/intro.md index b2a41c45b7..b4c19f7dbf 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -12,3 +12,14 @@ It is also built on top of the newer Microsoft Testing Platform, which was rewri :::performance TUnit is designed for speed. Through source generation and compile-time optimizations, TUnit significantly outperforms traditional testing frameworks. See the [performance benchmarks](/docs/benchmarks) for real-world speed comparisons. ::: + +## What's in These Docs + +- **[Getting Started](getting-started/installation.md)** — Install TUnit, write your first test, and run it +- **[Writing Tests](writing-tests/things-to-know.md)** — Test attributes, data-driven testing, lifecycle hooks, and dependency injection +- **[Assertions](assertions/getting-started.md)** — Fluent assertion syntax for values, collections, strings, exceptions, and more +- **[Execution](execution/parallelism.md)** — Control parallelism, ordering, retries, and timeouts +- **[Extending TUnit](extending/built-in-extensions.md)** — Built-in extensions, custom data sources, and event subscribers +- **[Migration](migration/xunit.md)** — Guides for switching from xUnit, NUnit, or MSTest +- **[Comparison](comparison/framework-differences.md)** — Feature comparisons with other frameworks +- **[Guides](guides/best-practices.md)** — Best practices, cookbook recipes, and philosophy diff --git a/docs/docs/writing-tests/culture.md b/docs/docs/writing-tests/culture.md index 2b3bd78662..53a03e3025 100644 --- a/docs/docs/writing-tests/culture.md +++ b/docs/docs/writing-tests/culture.md @@ -1,17 +1,38 @@ # Culture -The `[Culture]` attribute is used to set the [current Culture](https://learn.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.currentculture) for the duration of a test. It may be specified at the level of a test, fixture or assembly. -The culture remains set until the test or fixture completes and is then reset to its original value. +The `[Culture]` attribute sets the [current Culture](https://learn.microsoft.com/en-us/dotnet/api/system.globalization.cultureinfo.currentculture) for the duration of a test. It can be applied at the test, class, or assembly level. The culture is restored to its original value when the test completes. -Specifying the culture is useful for comparing against expected output -that depends on the culture, e.g. decimal separators, etc. +## Why This Matters -Only one culture may be specified. If you wish to run the same test under multiple cultures, -you can achieve the same result by factoring out your test code into a private method -that is called by each individual test method. +Different locales format numbers, dates, and currencies differently. A test that passes on a developer's machine in the US may fail on a CI server in Germany if it parses or compares formatted strings. The `[Culture]` attribute locks the culture so results are predictable regardless of where the tests run. + +**Without `[Culture]`** — this test passes in `en-US` but fails in `de-AT` (where the decimal separator is `,`): + +```csharp +[Test] +public async Task Fragile_Without_Culture() +{ + // Fails in locales where "3.5" is not a valid double literal + var value = double.Parse("3.5"); + await Assert.That(value).IsEqualTo(3.5); +} +``` + +**With `[Culture]`** — the test is stable everywhere: + +```csharp +[Test, Culture("en-US")] +public async Task Stable_With_Culture() +{ + var value = double.Parse("3.5"); + await Assert.That(value).IsEqualTo(3.5); +} +``` ## Examples +### Test-Level Culture + ```csharp using TUnit.Core; @@ -20,9 +41,43 @@ namespace MyTestProject; public class MyTestClass { [Test, Culture("de-AT")] - public async Task Test3() + public async Task Parse_German_Decimal() { await Assert.That(double.Parse("3,5")).IsEqualTo(3.5); } } ``` + +### Class-Level Culture + +Apply the attribute to the class to set the culture for every test in that fixture: + +```csharp +[Culture("fr-FR")] +public class FrenchFormattingTests +{ + [Test] + public async Task Currency_Format() + { + var formatted = 1234.56.ToString("C"); + await Assert.That(formatted).Contains("1"); + } +} +``` + +### Assembly-Level Culture + +Lock the culture for the entire test assembly: + +```csharp +[assembly: Culture("en-US")] +``` + +## Notes + +- Only one culture can be specified per scope. To run the same test under multiple cultures, factor out the test logic into a private method and call it from separate test methods, each with its own `[Culture]` attribute. +- The attribute sets both `CurrentCulture` and `CurrentUICulture` for the executing thread. + +## See Also + +- [Command-Line Flags](../reference/command-line-flags.md) — Runtime configuration options diff --git a/docs/docs/writing-tests/things-to-know.md b/docs/docs/writing-tests/things-to-know.md index f9f6916e08..e8330be916 100644 --- a/docs/docs/writing-tests/things-to-know.md +++ b/docs/docs/writing-tests/things-to-know.md @@ -81,3 +81,9 @@ public class MyTests + +## See Also + +- [Test Lifecycle](lifecycle.md) — Understand the order of setup, execution, and cleanup +- [Hooks & Setup](hooks-setup.md) — Run code before tests, classes, or assemblies +- [Controlling Parallelism](../execution/parallelism.md) — Configure how tests run in parallel diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index e71162bd68..0061f55270 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -88,7 +88,7 @@ const config: Config = { type: 'docSidebar', sidebarId: 'docs', position: 'left', - label: 'Tutorial', + label: 'Docs', }, { href: 'https://github.com/thomhurst/TUnit/issues', @@ -125,7 +125,7 @@ const config: Config = { title: 'Docs', items: [ { - label: 'Tutorial', + label: 'Docs', to: '/docs/intro', }, ], diff --git a/docs/sidebars.ts b/docs/sidebars.ts index e124d26e61..b60a89c220 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -83,7 +83,7 @@ const sidebars: SidebarsConfig = { collapsed: true, items: [ 'assertions/getting-started', - 'assertions/library', + 'assertions/awaiting', { type: 'category', label: 'Value Assertions', @@ -113,7 +113,6 @@ const sidebars: SidebarsConfig = { label: 'Async & Exceptions', collapsed: true, items: [ - 'assertions/awaiting', 'assertions/tasks-and-async', 'assertions/exceptions', 'assertions/delegates', @@ -135,6 +134,7 @@ const sidebars: SidebarsConfig = { 'assertions/extensibility/extensibility-returning-items-from-await', ], }, + 'assertions/library', ], }, { @@ -146,16 +146,7 @@ const sidebars: SidebarsConfig = { 'execution/timeouts', 'execution/retrying', 'execution/repeating', - { - type: 'category', - label: 'Parallelism', - collapsed: true, - items: [ - 'execution/not-in-parallel', - 'execution/parallel-groups', - 'execution/parallel-limiter', - ], - }, + 'execution/parallelism', 'execution/ci-cd-reporting', 'execution/engine-modes', ], diff --git a/docs/src/components/ChooseYourJourney/index.tsx b/docs/src/components/ChooseYourJourney/index.tsx index 7873c8115f..3b33bf0f0a 100644 --- a/docs/src/components/ChooseYourJourney/index.tsx +++ b/docs/src/components/ChooseYourJourney/index.tsx @@ -32,7 +32,7 @@ const journeys: JourneyCard[] = [ icon: '🔄', color: '#3b82f6', links: [ - { label: 'Migration Overview', href: '/docs/migration/testcontext-interface-organization' }, + { label: 'Migration Overview', href: '/docs/comparison/framework-differences' }, { label: 'From xUnit', href: '/docs/migration/xunit' }, { label: 'From NUnit', href: '/docs/migration/nunit' }, { label: 'From MSTest', href: '/docs/migration/mstest' }, @@ -47,7 +47,7 @@ const journeys: JourneyCard[] = [ { label: 'Assertions Library', href: '/docs/assertions/library' }, { label: 'Assertion Basics', href: '/docs/assertions/getting-started' }, { label: 'Data Driven Testing', href: '/docs/writing-tests/arguments' }, - { label: 'Test Lifecycle', href: '/docs/writing-tests/hooks-setup' }, + { label: 'Test Lifecycle', href: '/docs/writing-tests/lifecycle' }, ], }, { @@ -58,7 +58,7 @@ const journeys: JourneyCard[] = [ links: [ { label: 'Best Practices', href: '/docs/guides/best-practices' }, { label: 'Performance Benchmarks', href: '/docs/benchmarks' }, - { label: 'Parallel Execution', href: '/docs/execution/not-in-parallel' }, + { label: 'Parallel Execution', href: '/docs/execution/parallelism' }, { label: 'Customization', href: '/docs/extending/data-source-generators' }, ], }, diff --git a/docs/src/components/HomepageFeatures/index.tsx b/docs/src/components/HomepageFeatures/index.tsx index 60fe382eba..b5af84e948 100644 --- a/docs/src/components/HomepageFeatures/index.tsx +++ b/docs/src/components/HomepageFeatures/index.tsx @@ -22,9 +22,9 @@ const FeatureList: FeatureItem[] = [ codeExample: `[Test] [Arguments(1, 2, 3)] [Arguments(4, 5, 9)] -public void TestAdd(int a, int b, int expected) +public async Task TestAdd(int a, int b, int expected) { - Assert.That(a + b).IsEqualTo(expected); + await Assert.That(a + b).IsEqualTo(expected); }` }, { @@ -54,11 +54,11 @@ public async Task TestAsync() ), codeExample: `// AOT Compatible [Test] -public void PerformantTest() +public async Task PerformantTest() { // Source generated // No reflection overhead - Assert.That(true).IsTrue(); + await Assert.That(true).IsTrue(); }` }, ]; diff --git a/docs/static/img/docusaurus.png b/docs/static/img/docusaurus.png deleted file mode 100644 index f458149e3c8f53335f28fbc162ae67f55575c881..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5142 zcma)=cTf{R(}xj7f`AaDml%oxrAm_`5IRVc-jPtHML-0kDIiip57LWD@4bW~(nB|) z34|^sbOZqj<;8ct`Tl-)=Jw`pZtiw=e$UR_Mn2b8rM$y@hlq%XQe90+?|Mf68-Ux_ zzTBiDn~3P%oVt>{f$z+YC7A)8ak`PktoIXDkpXod+*gQW4fxTWh!EyR9`L|fi4YlH z{IyM;2-~t3s~J-KF~r-Z)FWquQCfG*TQy6w*9#k2zUWV-+tCNvjrtl9(o}V>-)N!) ziZgEgV>EG+b(j@ex!dx5@@nGZim*UfFe<+e;(xL|j-Pxg(PCsTL~f^br)4{n5?OU@ z*pjt{4tG{qBcDSa3;yKlopENd6Yth=+h9)*lkjQ0NwgOOP+5Xf?SEh$x6@l@ZoHoYGc5~d2>pO43s3R|*yZw9yX^kEyUV2Zw1%J4o`X!BX>CwJ zI8rh1-NLH^x1LnaPGki_t#4PEz$ad+hO^$MZ2 ziwt&AR}7_yq-9Pfn}k3`k~dKCbOsHjvWjnLsP1{)rzE8ERxayy?~{Qz zHneZ2gWT3P|H)fmp>vA78a{0&2kk3H1j|n59y{z@$?jmk9yptqCO%* zD2!3GHNEgPX=&Ibw?oU1>RSxw3;hhbOV77-BiL%qQb1(4J|k=Y{dani#g>=Mr?Uyd z)1v~ZXO_LT-*RcG%;i|Wy)MvnBrshlQoPxoO*82pKnFSGNKWrb?$S$4x+24tUdpb= zr$c3K25wQNUku5VG@A=`$K7%?N*K+NUJ(%%)m0Vhwis*iokN#atyu(BbK?+J+=H z!kaHkFGk+qz`uVgAc600d#i}WSs|mtlkuwPvFp) z1{Z%nt|NwDEKj1(dhQ}GRvIj4W?ipD76jZI!PGjd&~AXwLK*98QMwN&+dQN1ML(6< z@+{1`=aIc z9Buqm97vy3RML|NsM@A>Nw2=sY_3Ckk|s;tdn>rf-@Ke1m!%F(9(3>V%L?w#O&>yn z(*VIm;%bgezYB;xRq4?rY})aTRm>+RL&*%2-B%m; zLtxLTBS=G!bC$q;FQ|K3{nrj1fUp`43Qs&V!b%rTVfxlDGsIt3}n4p;1%Llj5ePpI^R} zl$Jhx@E}aetLO!;q+JH@hmelqg-f}8U=XnQ+~$9RHGUDOoR*fR{io*)KtYig%OR|08ygwX%UqtW81b@z0*`csGluzh_lBP=ls#1bwW4^BTl)hd|IIfa zhg|*M%$yt@AP{JD8y!7kCtTmu{`YWw7T1}Xlr;YJTU1mOdaAMD172T8Mw#UaJa1>V zQ6CD0wy9NEwUsor-+y)yc|Vv|H^WENyoa^fWWX zwJz@xTHtfdhF5>*T70(VFGX#8DU<^Z4Gez7vn&4E<1=rdNb_pj@0?Qz?}k;I6qz@| zYdWfcA4tmI@bL5JcXuoOWp?ROVe*&o-T!><4Ie9@ypDc!^X&41u(dFc$K$;Tv$c*o zT1#8mGWI8xj|Hq+)#h5JToW#jXJ73cpG-UE^tsRf4gKw>&%Z9A>q8eFGC zG@Iv(?40^HFuC_-%@u`HLx@*ReU5KC9NZ)bkS|ZWVy|_{BOnlK)(Gc+eYiFpMX>!# zG08xle)tntYZ9b!J8|4H&jaV3oO(-iFqB=d}hGKk0 z%j)johTZhTBE|B-xdinS&8MD=XE2ktMUX8z#eaqyU?jL~PXEKv!^) zeJ~h#R{@O93#A4KC`8@k8N$T3H8EV^E2 z+FWxb6opZnX-av5ojt@`l3TvSZtYLQqjps{v;ig5fDo^}{VP=L0|uiRB@4ww$Eh!CC;75L%7|4}xN+E)3K&^qwJizphcnn=#f<&Np$`Ny%S)1*YJ`#@b_n4q zi%3iZw8(I)Dzp0yY}&?<-`CzYM5Rp+@AZg?cn00DGhf=4|dBF8BO~2`M_My>pGtJwNt4OuQm+dkEVP4 z_f*)ZaG6@t4-!}fViGNd%E|2%ylnzr#x@C!CrZSitkHQ}?_;BKAIk|uW4Zv?_npjk z*f)ztC$Cj6O<_{K=dPwO)Z{I=o9z*lp?~wmeTTP^DMP*=<-CS z2FjPA5KC!wh2A)UzD-^v95}^^tT<4DG17#wa^C^Q`@f@=jLL_c3y8@>vXDJd6~KP( zurtqU1^(rnc=f5s($#IxlkpnU=ATr0jW`)TBlF5$sEwHLR_5VPTGiO?rSW9*ND`bYN*OX&?=>!@61{Z4)@E;VI9 zvz%NmR*tl>p-`xSPx$}4YcdRc{_9k)>4Jh&*TSISYu+Y!so!0JaFENVY3l1n*Fe3_ zRyPJ(CaQ-cNP^!3u-X6j&W5|vC1KU!-*8qCcT_rQN^&yqJ{C(T*`(!A=))=n%*-zp_ewRvYQoJBS7b~ zQlpFPqZXKCXUY3RT{%UFB`I-nJcW0M>1^*+v)AxD13~5#kfSkpWys^#*hu)tcd|VW zEbVTi`dbaM&U485c)8QG#2I#E#h)4Dz8zy8CLaq^W#kXdo0LH=ALhK{m_8N@Bj=Um zTmQOO*ID(;Xm}0kk`5nCInvbW9rs0pEw>zlO`ZzIGkB7e1Afs9<0Z(uS2g*BUMhp> z?XdMh^k}k<72>}p`Gxal3y7-QX&L{&Gf6-TKsE35Pv%1 z;bJcxPO+A9rPGsUs=rX(9^vydg2q`rU~otOJ37zb{Z{|)bAS!v3PQ5?l$+LkpGNJq zzXDLcS$vMy|9sIidXq$NE6A-^v@)Gs_x_3wYxF%y*_e{B6FvN-enGst&nq0z8Hl0< z*p6ZXC*su`M{y|Fv(Vih_F|83=)A6ay-v_&ph1Fqqcro{oeu99Y0*FVvRFmbFa@gs zJ*g%Gik{Sb+_zNNf?Qy7PTf@S*dTGt#O%a9WN1KVNj`q$1Qoiwd|y&_v?}bR#>fdP zSlMy2#KzRq4%?ywXh1w;U&=gKH%L~*m-l%D4Cl?*riF2~r*}ic9_{JYMAwcczTE`!Z z^KfriRf|_YcQ4b8NKi?9N7<4;PvvQQ}*4YxemKK3U-7i}ap8{T7=7`e>PN7BG-Ej;Uti2$o=4T#VPb zm1kISgGzj*b?Q^MSiLxj26ypcLY#RmTPp+1>9zDth7O?w9)onA%xqpXoKA-`Jh8cZ zGE(7763S3qHTKNOtXAUA$H;uhGv75UuBkyyD;eZxzIn6;Ye7JpRQ{-6>)ioiXj4Mr zUzfB1KxvI{ZsNj&UA`+|)~n}96q%_xKV~rs?k=#*r*7%Xs^Hm*0~x>VhuOJh<2tcb zKbO9e-w3zbekha5!N@JhQm7;_X+J!|P?WhssrMv5fnQh$v*986uWGGtS}^szWaJ*W z6fLVt?OpPMD+-_(3x8Ra^sX~PT1t5S6bfk@Jb~f-V)jHRul#Hqu;0(+ER7Z(Z4MTR z+iG>bu+BW2SNh|RAGR2-mN5D1sTcb-rLTha*@1@>P~u;|#2N{^AC1hxMQ|(sp3gTa zDO-E8Yn@S7u=a?iZ!&&Qf2KKKk7IT`HjO`U*j1~Df9Uxz$~@otSCK;)lbLSmBuIj% zPl&YEoRwsk$8~Az>>djrdtp`PX z`Pu#IITS7lw07vx>YE<4pQ!&Z^7L?{Uox`CJnGjYLh1XN^tt#zY*0}tA*a=V)rf=&-kLgD|;t1D|ORVY}8 F{0H{b<4^zq diff --git a/docs/static/img/easy.svg b/docs/static/img/easy.svg deleted file mode 100644 index bdc73ca86c..0000000000 --- a/docs/static/img/easy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/static/img/fast.svg b/docs/static/img/fast.svg deleted file mode 100644 index 6fd26bad30..0000000000 --- a/docs/static/img/fast.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/static/img/favicon.ico b/docs/static/img/favicon.ico deleted file mode 100644 index c01d54bcd39a5f853428f3cd5aa0f383d963c484..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3626 zcmb`Je@s(X6vrR`EK3%b%orErlDW({vnABqA zcfaS{d+xbU5JKp0*;0YOg+;Fl!eT)XRuapIwFLL`=imZCSon$`se`_<%@MB=M~KG+ z=EW^FL`w|Bo>*ktlaS^(fut!95`iG5u=SZ8nfDHO#GaTlH1-XG^;vsjUb^gWTVz0+ z^=WR1wv9-2oeR=_;fL0H7rNWqAzGtO(D;`~cX(RcN0w2v24Y8)6t`cS^_ghs`_ho? z{0ka~1Dgo8TfAP$r*ua?>$_V+kZ!-(TvEJ7O2f;Y#tezt$&R4 zLI}=-y@Z!grf*h3>}DUL{km4R>ya_I5Ag#{h_&?+HpKS!;$x3LC#CqUQ8&nM?X))Q zXAy2?`YL4FbC5CgJu(M&Q|>1st8XXLZ|5MgwgjP$m_2Vt0(J z&Gu7bOlkbGzGm2sh?X`){7w69Y$1#@P@7DF{ZE=4%T0NDS)iH`tiPSKpDNW)zmtn( zw;4$f>k)4$LBc>eBAaTZeCM2(iD+sHlj!qd z2GjRJ>f_Qes(+mnzdA^NH?^NB(^o-%Gmg$c8MNMq&`vm@9Ut;*&$xSD)PKH{wBCEC z4P9%NQ;n2s59ffMn8*5)5AAg4-93gBXBDX`A7S& zH-|%S3Wd%T79fk-e&l`{!?lve8_epXhE{d3Hn$Cg!t=-4D(t$cK~7f&4s?t7wr3ZP z*!SRQ-+tr|e1|hbc__J`k3S!rMy<0PHy&R`v#aJv?`Y?2{avK5sQz%=Us()jcNuZV z*$>auD4cEw>;t`+m>h?f?%VFJZj8D|Y1e_SjxG%J4{-AkFtT2+ZZS5UScS~%;dp!V>)7zi`w(xwSd*FS;Lml=f6hn#jq)2is4nkp+aTrV?)F6N z>DY#SU0IZ;*?Hu%tSj4edd~kYNHMFvS&5}#3-M;mBCOCZL3&;2obdG?qZ>rD|zC|Lu|sny76pn2xl|6sk~Hs{X9{8iBW zwiwgQt+@hi`FYMEhX2 \ No newline at end of file diff --git a/docs/static/img/lab.svg b/docs/static/img/lab.svg deleted file mode 100644 index eaf1a7a7f9..0000000000 --- a/docs/static/img/lab.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/static/img/logo.svg b/docs/static/img/logo.svg deleted file mode 100644 index 9db6d0d066..0000000000 --- a/docs/static/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/docs/static/img/undraw_docusaurus_mountain.svg b/docs/static/img/undraw_docusaurus_mountain.svg deleted file mode 100644 index af961c49a8..0000000000 --- a/docs/static/img/undraw_docusaurus_mountain.svg +++ /dev/null @@ -1,171 +0,0 @@ - - Easy to Use - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/static/img/undraw_docusaurus_react.svg b/docs/static/img/undraw_docusaurus_react.svg deleted file mode 100644 index 94b5cf08f8..0000000000 --- a/docs/static/img/undraw_docusaurus_react.svg +++ /dev/null @@ -1,170 +0,0 @@ - - Powered by React - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/static/img/undraw_docusaurus_tree.svg b/docs/static/img/undraw_docusaurus_tree.svg deleted file mode 100644 index d9161d3392..0000000000 --- a/docs/static/img/undraw_docusaurus_tree.svg +++ /dev/null @@ -1,40 +0,0 @@ - - Focus on What Matters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 6d96287151558b173a8e6674ae8513bbc8478bcf Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Thu, 26 Feb 2026 23:14:19 +0000 Subject: [PATCH 9/9] docs: fix incorrect API signatures flagged in PR review - Fix TestExecutionException constructor: use (Exception?, [], []) instead of non-existent (string, Exception) overload - Fix IClassConstructor.Create signature: non-generic Task with Type parameter, not generic Create - Add minimal single-item Func example to method-data-source.md --- docs/docs/extending/exception-handling.md | 6 ++---- .../writing-tests/dependency-injection.md | 11 +++++----- docs/docs/writing-tests/method-data-source.md | 20 +++++++++++++++++++ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/docs/extending/exception-handling.md b/docs/docs/extending/exception-handling.md index e80297dd1e..e721688b1a 100644 --- a/docs/docs/extending/exception-handling.md +++ b/docs/docs/extending/exception-handling.md @@ -224,10 +224,8 @@ public class SafeTestExecutor : ITestExecutor } catch (Exception ex) { - // Wrap other exceptions with context - throw new TestExecutionException( - $"Test '{context.Metadata.TestName}' failed with unexpected exception", - ex); + // Wrap other exceptions in a TestExecutionException + throw new TestExecutionException(ex, [], []); } } } diff --git a/docs/docs/writing-tests/dependency-injection.md b/docs/docs/writing-tests/dependency-injection.md index c0390b1fab..053cbe040c 100644 --- a/docs/docs/writing-tests/dependency-injection.md +++ b/docs/docs/writing-tests/dependency-injection.md @@ -11,11 +11,12 @@ Register it with `[ClassConstructor]` on the test class. Each test gets its o ```csharp public class CustomConstructor : IClassConstructor { - public T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] T>( - ClassConstructorMetadata classConstructorMetadata) where T : class + public Task Create( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, + ClassConstructorMetadata classConstructorMetadata) { - // Resolve T however you like — manual construction, a container, etc. - return Activator.CreateInstance(); + // Resolve the type however you like — manual construction, a container, etc. + return Task.FromResult(Activator.CreateInstance(type)!); } } @@ -25,7 +26,7 @@ public class MyTestClass(SomeDependency dep) [Test] public async Task MyTest() { - // dep was provided by CustomConstructor.Create() + // dep was provided by CustomConstructor.Create() } } ``` diff --git a/docs/docs/writing-tests/method-data-source.md b/docs/docs/writing-tests/method-data-source.md index 7e0ec316da..7b52ecf058 100644 --- a/docs/docs/writing-tests/method-data-source.md +++ b/docs/docs/writing-tests/method-data-source.md @@ -23,6 +23,26 @@ Returning a `Func` ensures that each test gets a fresh object. If you return a reference to the same object, tests may interfere with each other. ::: +Here's the simplest example — a method that returns a single `Func`, creating one test case: + +```csharp +public static class MyTestDataSources +{ + public static Func SingleTestCase() + => () => new AdditionTestData(1, 2, 3); +} + +public class MyTestClass +{ + [Test] + [MethodDataSource(typeof(MyTestDataSources), nameof(MyTestDataSources.SingleTestCase))] + public async Task MyTest(AdditionTestData data) + { + await Assert.That(data.Value1 + data.Value2).IsEqualTo(data.ExpectedResult); + } +} +``` + Return an `IEnumerable>` to generate multiple test cases. For each item returned, a new test will be created with that item passed in to the parameters. Each `Func<>` should return a `new T()` so every test gets its own instance. Here's an example using a record type (the test is invoked 3 times):