diff --git a/.claude/docs/workflows.md b/.claude/docs/workflows.md index 845d58a47f4..601ad030aaf 100644 --- a/.claude/docs/workflows.md +++ b/.claude/docs/workflows.md @@ -141,11 +141,13 @@ Use a directive immediately before a fence when its context needs to be explicit - - ``` -Use `` on each fence that depends on surrounding prose. Its page must declare ``, so unmarked fences still compile and cannot be silently excluded by page-level configuration. Use `` when no snippets on a page can compile independently. File directives require a reason. +Every C# fence must compile with warnings as errors. Failure-masking ignore/contextual directives, +warning pragmas, nullable disabling, `#if false`, and suppression attributes are rejected. The verifier +also fails unless `NoWarn` and `WarningsNotAsErrors` are empty. + +Use `` once on tutorial pages whose fences intentionally share declarations. Every fence still compiles; generated snippets use one page-scoped namespace. For a fence that mixes declarations or members with usage, split it explicitly at an exact marker: diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 46d4a9b49a5..8ed4fb8b551 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -10,14 +10,14 @@ "rollForward": false }, "verify.tool": { - "version": "0.8.0", + "version": "0.9.1", "commands": [ "dotnet-verify" ], "rollForward": true }, "dotnet-trace": { - "version": "9.0.661903", + "version": "10.0.731102", "commands": [ "dotnet-trace" ], diff --git a/Directory.Packages.props b/Directory.Packages.props index 2d60d171643..72d8104e6be 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,10 @@ true + + + + @@ -11,6 +15,7 @@ + @@ -41,29 +46,31 @@ - + + - - - + + + + - - - - - + + + + + - - - + + + @@ -71,6 +78,8 @@ + + @@ -85,6 +94,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -103,14 +113,14 @@ - + - - - - - + + + + + diff --git a/README.md b/README.md index f8e829874c5..222d42adbc0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ ![TUnit](assets/banner.png) - # TUnit @@ -17,7 +16,6 @@ A modern .NET testing framework. Tests are discovered at compile time via source ## What it looks like - ```csharp [Test] [Arguments("GOLD", 100.00, 80.00)] @@ -64,14 +62,14 @@ Source generation shifts work from run time to build time: you pay a little up f | Scenario | TUnit (AOT) | TUnit | xUnit v3 | NUnit | MSTest | |----------|---|---|---|---|---| -| Data-driven tests | 13.98 ms | 268.40 ms | 586.26 ms | 498.74 ms | 490.02 ms | -| Async-heavy tests | 118.5 ms | 358.9 ms | 737.0 ms | 577.1 ms | 678.0 ms | -| Matrix combinations | 120.4 ms | 377.3 ms | 960.5 ms | 1,443.4 ms | 1,532.2 ms | -| Large suites (scale) | 19.86 ms | 280.02 ms | 620.62 ms | 522.04 ms | 505.40 ms | -| Massive parallelism | 218.2 ms | 471.8 ms | 1,289.6 ms | 1,083.0 ms | 2,975.1 ms | -| Setup/teardown lifecycle | 70.18 ms | 389.40 ms | 784.00 ms | 1,090.23 ms | 1,163.10 ms | - -Mean wall-clock time to run the same test suite. TUnit (AOT) 1.65.38 · TUnit 1.65.38 · xUnit v3 4.0.0 · NUnit 4.6.1 · MSTest 4.3.3. .NET SDK 10.0.400, .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4. Updated 2026-08-23 — regenerated weekly by the [Speed Comparison workflow](https://github.com/thomhurst/TUnit/actions/workflows/speed-comparison.yml). Full results and methodology: [tunit.dev/docs/benchmarks](https://tunit.dev/docs/benchmarks/). +| Data-driven tests | 16.57 ms | 281.32 ms | 655.45 ms | 564.15 ms | 507.18 ms | +| Async-heavy tests | 116.0 ms | 388.5 ms | 730.9 ms | 714.3 ms | 664.8 ms | +| Matrix combinations | 117.1 ms | 364.9 ms | 861.9 ms | 1,536.5 ms | 1,497.2 ms | +| Large suites (scale) | 18.88 ms | 334.54 ms | 710.91 ms | 643.81 ms | 562.39 ms | +| Massive parallelism | 220.9 ms | 535.8 ms | 1,337.9 ms | 1,317.5 ms | 3,040.5 ms | +| Setup/teardown lifecycle | 75.64 ms | 367.81 ms | 955.39 ms | 1,270.05 ms | 1,322.57 ms | + +Mean wall-clock time to run the same test suite. TUnit (AOT) 1.65.68 · TUnit 1.65.68 · xUnit v3 4.0.0 · NUnit 4.6.1 · MSTest 4.3.3. .NET SDK 10.0.400, .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4. Updated 2026-08-30 — regenerated weekly by the [Speed Comparison workflow](https://github.com/thomhurst/TUnit/actions/workflows/speed-comparison.yml). Full results and methodology: [tunit.dev/docs/benchmarks](https://tunit.dev/docs/benchmarks/). ## Getting Started @@ -97,7 +95,6 @@ dotnet add package TUnit ### Data-driven tests - ```csharp [Test] [Arguments("user1@test.com", "ValidPassword123")] @@ -118,7 +115,6 @@ Need more? `[MethodDataSource]` pulls rows from a method, and custom `DataSource Assertions are async, chainable, and produce the focused failure messages shown above: - ```csharp await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK) .Because("the health endpoint should always be up"); @@ -161,13 +157,12 @@ public class OrderRepositoryTests } ``` -Property injection keeps base test classes clean — subclasses inherit the fixture without re-threading constructor parameters. Prefer a constructor param? That works too. Disposal is reference-counted, so shared fixtures are torn down exactly when the last test using them finishes. +Property injection keeps base test classes clean — subclasses inherit the fixture without re-threading constructor parameters. Prefer a constructor parameter on the test class? That works too. Types created by `ClassDataSource` require a public parameterless constructor; use property injection for their nested dependencies. Disposal is reference-counted, so shared fixtures are torn down exactly when the last test using them finishes. ### Parallelism you control Everything runs in parallel by default. Opt out or sequence tests where it matters: - ```csharp [Test] public async Task Register_User() { ... } @@ -184,7 +179,6 @@ public async Task Migrates_Schema() { ... } ### Lifecycle hooks at every scope - ```csharp [Before(Test)] // also: Class, Assembly, TestSession public async Task SetUp() { ... } @@ -197,13 +191,13 @@ public static async Task TearDownDatabase(ClassHookContext context) { ... } `TUnit.Mocks` is a source-generated, Native AOT-compatible mocking library — no runtime proxies, no `Castle.Core`. It works with any test framework: - ```csharp var gateway = IPaymentGateway.Mock(); // or Mock.Of() gateway.ChargeAsync(Any()).Returns(new ChargeResult(Success: true)); var checkout = new CheckoutService(gateway.Object); +var cart = new Cart(99.99m); await checkout.CompleteAsync(cart); gateway.ChargeAsync(99.99m).WasCalled(Times.Once); @@ -211,7 +205,6 @@ gateway.ChargeAsync(99.99m).WasCalled(Times.Once); Companion packages mock the annoying stuff for you: - ```csharp // TUnit.Mocks.Http — a real HttpClient backed by a scriptable handler using var client = Mock.HttpClient("https://api.example.com"); @@ -219,14 +212,13 @@ client.Handler.OnGet("/users/1").RespondWithJson("""{ "id": 1 }"""); // TUnit.Mocks.Logging — capture and verify ILogger output var logger = Mock.Logger(); -logger.VerifyLog().AtLevel(LogLevel.Warning).ContainingMessage("retrying").WasCalled(Times.Once); +logger.VerifyLog().AtLevel(Microsoft.Extensions.Logging.LogLevel.Warning).ContainingMessage("retrying").WasCalled(Times.Once); ``` ### Custom attributes Extend built-in base classes to create your own skip conditions, retry logic, and more: - ```csharp public class WindowsOnlyAttribute : SkipAttribute { @@ -265,7 +257,6 @@ public class HealthCheckTests(ApiFactory factory) Spin up your whole distributed app once per test session, with resource log forwarding and OpenTelemetry capture built in: - ```csharp public class AppFixture : AspireFixture; @@ -302,7 +293,6 @@ public class HomePageTests : PageTest ### Property-based testing (FsCheck) - ```csharp [Test, FsCheckProperty] public bool Reversing_Twice_Returns_Original(int[] array) => diff --git a/docs/docs/assertions/awaiting.md b/docs/docs/assertions/awaiting.md index 0ca0c7457a5..53837f6bb5e 100644 --- a/docs/docs/assertions/awaiting.md +++ b/docs/docs/assertions/awaiting.md @@ -2,7 +2,6 @@ sidebar_position: 1 --- - # Awaiting @@ -17,7 +16,6 @@ If you forget to `await`, your assertion will not actually be executed, and your This will error: - ```csharp [Test] public void MyTest() @@ -30,7 +28,6 @@ This will error: This won't: - ```csharp [Test] public async Task MyTest() @@ -50,7 +47,6 @@ When you `await` an assertion in TUnit, it returns a reference to the subject th ### Type Casting with Confidence - ```csharp [Test] public async Task CastAndUseSpecificType() @@ -61,7 +57,7 @@ public async Task CastAndUseSpecificType() var circle = await Assert.That(shape).IsTypeOf(); // Now you can use circle-specific properties without casting - await Assert.That(circle.Radius).IsEqualTo(5.0); + await Assert.That(circle!.Radius).IsEqualTo(5.0); var area = Math.PI * circle.Radius * circle.Radius; await Assert.That(area).IsEqualTo(Math.PI * 25).Within(0.0001); @@ -74,7 +70,6 @@ public async Task CastAndUseSpecificType() You can chain multiple assertions together for more complex validations: - ```csharp [Test] public async Task ComplexObjectValidation() @@ -92,7 +87,6 @@ public async Task ComplexObjectValidation() ### Collection Assertions with Complex Conditions - ```csharp [Test] public async Task ComplexCollectionAssertions() @@ -116,7 +110,6 @@ public async Task ComplexCollectionAssertions() ### Async Operation Assertions - ```csharp [Test] public async Task AsyncOperationAssertions() @@ -138,7 +131,6 @@ public async Task AsyncOperationAssertions() ### Exception Assertions with Details - ```csharp [Test] public async Task DetailedExceptionAssertions() @@ -151,7 +143,7 @@ public async Task DetailedExceptionAssertions() .WithMessage("Validation failed"); // Assert ArgumentException with parameter name - await Assert.That(() => ProcessInvalidData(null)) + await Assert.That(() => ProcessInvalidData((object?)null)) .Throws() .WithParameterName("data"); @@ -159,14 +151,13 @@ public async Task DetailedExceptionAssertions() var exception = await Assert.That(() => ParallelOperationAsync()) .Throws(); - await Assert.That(exception.InnerExceptions).Count().IsEqualTo(3); + await Assert.That(exception!.InnerExceptions).Count().IsEqualTo(3); await Assert.That(exception.InnerExceptions).All(e => e is TaskCanceledException); } ``` ### Custom Assertion Conditions - ```csharp [Test] public async Task CustomAssertionConditions() @@ -176,8 +167,8 @@ public async Task CustomAssertionConditions() // Use custom conditions for complex validations await Assert.That(measurements) .Satisfies(m => { - var average = m.Average(); - var stdDev = CalculateStandardDeviation(m); + var average = m!.Average(); + var stdDev = CalculateStandardDeviation(m!); return stdDev < average * 0.1; // Less than 10% deviation }, "Measurements should have low standard deviation"); @@ -191,7 +182,6 @@ public async Task CustomAssertionConditions() ### Combining Or and And Conditions - ```csharp [Test] public async Task ComplexLogicalConditions() diff --git a/docs/docs/assertions/boolean.md b/docs/docs/assertions/boolean.md index f1f87a2540d..ddd3aae7d10 100644 --- a/docs/docs/assertions/boolean.md +++ b/docs/docs/assertions/boolean.md @@ -2,7 +2,6 @@ sidebar_position: 3.5 --- - # Boolean Assertions @@ -14,7 +13,6 @@ TUnit provides simple, expressive assertions for testing boolean values. These a Tests that a boolean value is `true`: - ```csharp [Test] public async Task Value_Is_True() @@ -31,7 +29,6 @@ public async Task Value_Is_True() Tests that a boolean value is `false`: - ```csharp [Test] public async Task Value_Is_False() @@ -39,7 +36,7 @@ public async Task Value_Is_False() var isExpired = CheckIfExpired(futureDate); await Assert.That(isExpired).IsFalse(); - var isEmpty = list.Count == 0; + var isEmpty = list.Length == 0; await Assert.That(isEmpty).IsFalse(); } ``` @@ -48,7 +45,6 @@ public async Task Value_Is_False() You can also use `IsEqualTo()` for boolean comparisons: - ```csharp [Test] public async Task Using_IsEqualTo() @@ -69,7 +65,6 @@ However, `IsTrue()` and `IsFalse()` are more expressive and recommended for bool Both assertions work with nullable booleans (`bool?`): - ```csharp [Test] public async Task Nullable_Boolean_True() @@ -98,7 +93,6 @@ public async Task Nullable_Boolean_False() If a nullable boolean is `null`, both `IsTrue()` and `IsFalse()` will fail: - ```csharp [Test] public async Task Nullable_Boolean_Null() @@ -118,7 +112,6 @@ public async Task Nullable_Boolean_Null() Boolean assertions can be chained with other assertions: - ```csharp [Test] public async Task Chained_With_Other_Assertions() @@ -135,22 +128,20 @@ public async Task Chained_With_Other_Assertions() ### Validation Results - ```csharp [Test] public async Task Email_Validation() { - var isValid = EmailValidator.Validate("test@example.com"); + var isValid = ValidateEmail("test@example.com"); await Assert.That(isValid).IsTrue(); - var isInvalid = EmailValidator.Validate("not-an-email"); + var isInvalid = ValidateEmail("not-an-email"); await Assert.That(isInvalid).IsFalse(); } ``` ### Permission Checks - ```csharp [Test] public async Task User_Permissions() @@ -165,12 +156,11 @@ public async Task User_Permissions() ### State Flags - ```csharp [Test] public async Task Service_State() { - var service = new BackgroundService(); + var service = new ExampleBackgroundService(); await Assert.That(service.IsRunning).IsFalse(); @@ -182,7 +172,6 @@ public async Task Service_State() ### Feature Flags - ```csharp [Test] public async Task Feature_Toggles() @@ -198,7 +187,6 @@ public async Task Feature_Toggles() When testing the boolean result of a comparison, use the specific assertion instead for clearer failure messages: - ```csharp [Test] public async Task Prefer_Specific_Assertions() diff --git a/docs/docs/assertions/collections.md b/docs/docs/assertions/collections.md index d6636f366fe..f18b6518547 100644 --- a/docs/docs/assertions/collections.md +++ b/docs/docs/assertions/collections.md @@ -2,7 +2,6 @@ sidebar_position: 6.5 --- - # Collection Assertions @@ -14,7 +13,6 @@ TUnit provides comprehensive assertions for testing collections, including membe Tests that a collection contains a specific item: - ```csharp [Test] public async Task Collection_Contains_Item() @@ -28,7 +26,6 @@ public async Task Collection_Contains_Item() Works with any collection type: - ```csharp [Test] public async Task Various_Collection_Types() @@ -48,7 +45,6 @@ public async Task Various_Collection_Types() Tests that a collection contains an item matching a predicate, and returns that item: - ```csharp [Test] public async Task Collection_Contains_Matching_Item() @@ -71,7 +67,6 @@ public async Task Collection_Contains_Matching_Item() Tests that a collection does not contain a specific item: - ```csharp [Test] public async Task Collection_Does_Not_Contain() @@ -87,7 +82,6 @@ public async Task Collection_Does_Not_Contain() Tests that no items match the predicate: - ```csharp [Test] public async Task Collection_Does_Not_Contain_Matching() @@ -109,7 +103,6 @@ public async Task Collection_Does_Not_Contain_Matching() Tests that a collection has an exact count: - ```csharp [Test] public async Task Collection_Has_Count() @@ -124,7 +117,6 @@ public async Task Collection_Has_Count() Get the count for further assertions: - ```csharp [Test] public async Task Count_With_Comparison() @@ -144,7 +136,6 @@ public async Task Count_With_Comparison() Count items that satisfy an assertion, allowing you to reuse existing assertion methods: - ```csharp [Test] public async Task Count_With_Inner_Assertion() @@ -178,7 +169,6 @@ public async Task Count_Strings_With_Inner_Assertion() Count assertions preserve the collection type, allowing you to chain additional collection assertions: - ```csharp [Test] public async Task Count_With_Chaining() @@ -199,9 +189,8 @@ public async Task Count_With_Chaining() // For non-int collections, you can also use inline count assertions var names = new[] { "Alice", "Bob", "Charlie" }; - await Assert.That(names) - .Count(c => c.IsEqualTo(3)) - .And.Contains("Bob"); + await Assert.That(names).Count().IsEqualTo(3); + await Assert.That(names).Contains("Bob"); } ``` @@ -209,7 +198,6 @@ public async Task Count_With_Chaining() Tests that a collection has no items: - ```csharp [Test] public async Task Collection_Is_Empty() @@ -225,7 +213,6 @@ public async Task Collection_Is_Empty() Tests that a collection has at least one item: - ```csharp [Test] public async Task Collection_Is_Not_Empty() @@ -240,7 +227,6 @@ public async Task Collection_Is_Not_Empty() Tests that a collection has exactly one item, and returns that item: - ```csharp [Test] public async Task Collection_Has_Single_Item() @@ -255,11 +241,9 @@ public async Task Collection_Has_Single_Item() Use `.Item` to continue assertions directly against the single item: - ```csharp -await Assert.That(users) - .HasSingleItem() - .Item.Member(user => user.Name, name => name.IsEqualTo("Alice")); +var user = await Assert.That(users).HasSingleItem(); +await Assert.That(user.Name).IsEqualTo("Alice"); ``` ## Ordering Assertions @@ -268,7 +252,6 @@ await Assert.That(users) Tests that a collection is sorted in ascending order: - ```csharp [Test] public async Task Collection_In_Ascending_Order() @@ -279,7 +262,6 @@ public async Task Collection_In_Ascending_Order() } ``` - ```csharp [Test] public async Task Strings_In_Order() @@ -294,7 +276,6 @@ public async Task Strings_In_Order() Tests that a collection is sorted in descending order: - ```csharp [Test] public async Task Collection_In_Descending_Order() @@ -309,7 +290,6 @@ public async Task Collection_In_Descending_Order() Tests that a collection is ordered by a specific property: - ```csharp [Test] public async Task Ordered_By_Property() @@ -329,7 +309,6 @@ public async Task Ordered_By_Property() Tests that a collection is ordered by a property in descending order: - ```csharp [Test] public async Task Ordered_By_Descending() @@ -351,7 +330,6 @@ public async Task Ordered_By_Descending() Tests that all items satisfy a condition: - ```csharp [Test] public async Task All_Items_Match() @@ -366,7 +344,6 @@ public async Task All_Items_Match() The single parameter overload will match T from `IEnumerable` - Giving you the relevant assertions for that type. - ```csharp [Test] public async Task All_Satisfy_With_Property() @@ -386,7 +363,6 @@ public async Task All_Satisfy_With_Property() You can also map to other types by accessing properties an such - And then assert on those specific values: - ```csharp [Test] public async Task All_Satisfy_With_Mapper() @@ -410,7 +386,6 @@ public async Task All_Satisfy_With_Mapper() Tests that at least one item satisfies a condition: - ```csharp [Test] public async Task Any_Item_Matches() @@ -429,7 +404,6 @@ Collection equivalency checks whether two collections contain the same elements. Tests that two collections contain the same items. By default, order is ignored (use `CollectionOrdering.Matching` to require matching order): - ```csharp [Test] public async Task Collections_Are_Equivalent() @@ -443,7 +417,6 @@ public async Task Collections_Are_Equivalent() Different collection types: - ```csharp [Test] public async Task Different_Collection_Types() @@ -457,7 +430,6 @@ public async Task Different_Collection_Types() #### With Custom Comparer - ```csharp [Test] public async Task Equivalent_With_Comparer() @@ -473,7 +445,6 @@ public async Task Equivalent_With_Comparer() #### With Custom Equality Predicate - ```csharp [Test] public async Task Equivalent_With_Predicate() @@ -492,7 +463,7 @@ public async Task Equivalent_With_Predicate() await Assert.That(users1) .IsEquivalentTo(users2) - .Using((u1, u2) => u1.Name == u2.Name && u1.Age == u2.Age); + .Using((u1, u2) => u1!.Name == u2!.Name && u1.Age == u2.Age); } ``` @@ -500,7 +471,6 @@ public async Task Equivalent_With_Predicate() By default, `IsEquivalentTo` ignores the order of elements: - ```csharp [Test] public async Task Equivalent_Ignoring_Order() @@ -517,7 +487,6 @@ public async Task Equivalent_Ignoring_Order() To require elements to be in the same order, pass `CollectionOrdering.Matching`: - ```csharp [Test] public async Task Equivalent_With_Matching_Order() @@ -531,7 +500,6 @@ public async Task Equivalent_With_Matching_Order() This will fail if elements are in different positions: - ```csharp [Test] public async Task Not_Equivalent_Different_Order() @@ -548,7 +516,6 @@ public async Task Not_Equivalent_Different_Order() Tests that collections are not equivalent: - ```csharp [Test] public async Task Collections_Not_Equivalent() @@ -570,7 +537,6 @@ The default behavior (ignoring order) is ideal for: - Checking API responses where element order doesn't matter - Testing collection transformations that may reorder elements - ```csharp [Test] public async Task Database_Query_Results() @@ -591,7 +557,6 @@ Use order-sensitive comparison when: - Checking sequences where position matters - Testing priority queues or ordered data structures - ```csharp [Test] public async Task Sorted_Query_Results() @@ -611,7 +576,6 @@ public async Task Sorted_Query_Results() If you need multiple order-sensitive assertions in the same test, consider extracting a helper or being explicit: - ```csharp [Test] public async Task Multiple_Order_Sensitive_Checks() @@ -627,7 +591,6 @@ public async Task Multiple_Order_Sensitive_Checks() For ordered comparisons, you can also use `IsInOrder()`: - ```csharp [Test] public async Task Verify_Ordering_Separately() @@ -646,7 +609,6 @@ public async Task Verify_Ordering_Separately() ### Deep Comparison with IsEquivalentTo - ```csharp [Test] public async Task Structurally_Equal() @@ -669,7 +631,6 @@ public async Task Structurally_Equal() ### IsNotEquivalentTo for Deep Comparison - ```csharp [Test] public async Task Not_Structurally_Equal() @@ -694,7 +655,6 @@ public async Task Not_Structurally_Equal() Tests that all items in a collection are unique: - ```csharp [Test] public async Task All_Items_Distinct() @@ -707,7 +667,6 @@ public async Task All_Items_Distinct() Fails if duplicates exist: - ```csharp [Test] public async Task Duplicates_Fail() @@ -723,7 +682,6 @@ public async Task Duplicates_Fail() ### Filtering Results - ```csharp [Test] public async Task Filter_And_Assert() @@ -739,7 +697,6 @@ public async Task Filter_And_Assert() ### LINQ Query Results - ```csharp [Test] public async Task LINQ_Query_Results() @@ -761,7 +718,6 @@ public async Task LINQ_Query_Results() ### Sorting Validation - ```csharp [Test] public async Task Verify_Sorting() @@ -776,7 +732,6 @@ public async Task Verify_Sorting() ### API Response Validation - ```csharp [Test] public async Task API_Returns_Expected_Items() @@ -785,14 +740,13 @@ public async Task API_Returns_Expected_Items() await Assert.That(response) .IsNotEmpty() - .And.All(u => u.Id > 0) + .And.All(u => u.Id is int id && id > 0) .And.All(u => !string.IsNullOrEmpty(u.Name)); } ``` ### Collection Transformation - ```csharp [Test] public async Task Map_And_Verify() @@ -815,7 +769,6 @@ public async Task Map_And_Verify() ## Empty vs Null Collections - ```csharp [Test] public async Task Empty_vs_Null() @@ -833,7 +786,6 @@ public async Task Empty_vs_Null() ## Nested Collections - ```csharp [Test] public async Task Nested_Collections() @@ -856,7 +808,6 @@ public async Task Nested_Collections() ## Chaining Collection Assertions - ```csharp [Test] public async Task Chained_Collection_Assertions() @@ -878,7 +829,6 @@ public async Task Chained_Collection_Assertions() ### Materialize IEnumerable - ```csharp [Test] public async Task Materialize_Before_Multiple_Assertions() @@ -898,7 +848,6 @@ public async Task Materialize_Before_Multiple_Assertions() ## Working with HashSet and SortedSet - ```csharp [Test] public async Task HashSet_Assertions() @@ -926,7 +875,6 @@ public async Task SortedSet_Assertions() ### Validate All Items - ```csharp [Test] public async Task Validate_Each_Item() @@ -946,7 +894,6 @@ public async Task Validate_Each_Item() Or more elegantly: - ```csharp [Test] public async Task Validate_All_With_Assertion() diff --git a/docs/docs/assertions/combining-assertions.md b/docs/docs/assertions/combining-assertions.md index a3a598ee027..1ef0b72a709 100644 --- a/docs/docs/assertions/combining-assertions.md +++ b/docs/docs/assertions/combining-assertions.md @@ -2,7 +2,6 @@ sidebar_position: 11 --- - # Combining Assertions @@ -12,7 +11,6 @@ TUnit provides several ways to combine multiple assertions within a single test: 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() @@ -20,8 +18,7 @@ public async Task MyTest() var result = Add(1, 2); await Assert.That(result) - .IsNotNull() - .And.IsPositive() + .IsPositive() .And.IsEqualTo(3); } ``` @@ -30,7 +27,6 @@ public async Task MyTest() 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() @@ -47,7 +43,6 @@ public async Task MyTest() :::warning Mixing And/Or is not supported `.And` and `.Or` cannot be mixed in a single chain. Attempting to use `.Or` after `.And` (or vice versa) throws `MixedAndOrAssertionsException` at runtime. If you need both kinds of logic, split the chain across multiple `Assert.That(...)` calls, or combine the conditions into a single boolean expression beforehand. - ```csharp // NOT supported - throws MixedAndOrAssertionsException at runtime await Assert.That(result) @@ -68,7 +63,6 @@ By default, a failing assertion throws immediately and stops the test. `Assert.M Implicit scope (covers the rest of the method): - ```csharp [Test] public async Task MyTest() @@ -84,7 +78,6 @@ public async Task MyTest() Explicit scope (covers only the block): - ```csharp [Test] public async Task MyTest() diff --git a/docs/docs/assertions/datetime.md b/docs/docs/assertions/datetime.md index d0a2cbb3e37..c4937e93483 100644 --- a/docs/docs/assertions/datetime.md +++ b/docs/docs/assertions/datetime.md @@ -2,7 +2,6 @@ sidebar_position: 7.5 --- - # DateTime and Time Assertions @@ -12,7 +11,6 @@ TUnit provides comprehensive assertions for date and time types, including `Date DateTime comparisons often need tolerance to account for timing variations: - ```csharp [Test] public async Task DateTime_With_Tolerance() @@ -30,7 +28,6 @@ public async Task DateTime_With_Tolerance() ### Tolerance Examples - ```csharp [Test] public async Task Various_Tolerance_Values() @@ -55,7 +52,6 @@ public async Task Various_Tolerance_Values() Standard comparison operators work with DateTime: - ```csharp [Test] public async Task DateTime_Comparison() @@ -74,7 +70,6 @@ public async Task DateTime_Comparison() ### IsToday / IsNotToday - ```csharp [Test] public async Task DateTime_Is_Today() @@ -92,7 +87,6 @@ public async Task DateTime_Is_Today() ### IsUtc / IsNotUtc - ```csharp [Test] public async Task DateTime_Kind() @@ -110,7 +104,6 @@ public async Task DateTime_Kind() ### IsLeapYear / IsNotLeapYear - ```csharp [Test] public async Task Leap_Year_Check() @@ -127,7 +120,6 @@ public async Task Leap_Year_Check() Compares against local time: - ```csharp [Test] public async Task Future_and_Past() @@ -144,7 +136,6 @@ public async Task Future_and_Past() Compares against UTC time: - ```csharp [Test] public async Task Future_and_Past_UTC() @@ -159,7 +150,6 @@ public async Task Future_and_Past_UTC() ### IsOnWeekend / IsOnWeekday - ```csharp [Test] public async Task Weekend_Check() @@ -169,13 +159,12 @@ public async Task Weekend_Check() var monday = new DateTime(2024, 1, 8); // Monday await Assert.That(monday).IsOnWeekday(); - await Assert.That(monday).IsNotOnWeekend(); + await Assert.That(monday.DayOfWeek is not DayOfWeek.Saturday and not DayOfWeek.Sunday).IsTrue(); } ``` ### IsDaylightSavingTime / IsNotDaylightSavingTime - ```csharp [Test] public async Task Daylight_Saving_Time() @@ -195,7 +184,6 @@ public async Task Daylight_Saving_Time() DateTimeOffset includes timezone information: - ```csharp [Test] public async Task DateTimeOffset_With_Tolerance() @@ -207,7 +195,6 @@ public async Task DateTimeOffset_With_Tolerance() } ``` - ```csharp [Test] public async Task DateTimeOffset_Comparison() @@ -224,7 +211,6 @@ public async Task DateTimeOffset_Comparison() DateOnly represents just a date without time: - ```csharp [Test] public async Task DateOnly_Assertions() @@ -238,7 +224,6 @@ public async Task DateOnly_Assertions() ### DateOnly with Days Tolerance - ```csharp [Test] public async Task DateOnly_With_Tolerance() @@ -252,7 +237,6 @@ public async Task DateOnly_With_Tolerance() ### DateOnly Comparison - ```csharp [Test] public async Task DateOnly_Comparison() @@ -269,7 +253,6 @@ public async Task DateOnly_Comparison() TimeOnly represents just time without a date: - ```csharp [Test] public async Task TimeOnly_Assertions() @@ -283,7 +266,6 @@ public async Task TimeOnly_Assertions() ### TimeOnly with Tolerance - ```csharp [Test] public async Task TimeOnly_With_Tolerance() @@ -299,7 +281,6 @@ public async Task TimeOnly_With_Tolerance() TimeSpan represents a duration: - ```csharp [Test] public async Task TimeSpan_Assertions() @@ -313,7 +294,6 @@ public async Task TimeSpan_Assertions() ### TimeSpan Comparison - ```csharp [Test] public async Task TimeSpan_Comparison() @@ -328,7 +308,6 @@ public async Task TimeSpan_Comparison() ### TimeSpan Sign Checks - ```csharp [Test] public async Task TimeSpan_Sign() @@ -345,7 +324,6 @@ public async Task TimeSpan_Sign() ### Expiration Checks - ```csharp [Test] public async Task Check_Token_Expiration() @@ -363,7 +341,6 @@ public async Task Check_Token_Expiration() ### Age Calculation - ```csharp [Test] public async Task Calculate_Age() @@ -383,7 +360,6 @@ public async Task Calculate_Age() ### Business Days - ```csharp [Test] public async Task Is_Business_Day() @@ -398,7 +374,6 @@ public async Task Is_Business_Day() ### Scheduling - ```csharp [Test] public async Task Scheduled_Time() @@ -413,7 +388,6 @@ public async Task Scheduled_Time() ### Performance Timing - ```csharp [Test] public async Task Operation_Duration() @@ -431,7 +405,6 @@ public async Task Operation_Duration() ### Date Range Validation - ```csharp [Test] public async Task Date_Within_Range() @@ -447,7 +420,6 @@ public async Task Date_Within_Range() ### Timestamp Validation - ```csharp [Test] public async Task Record_Created_Recently() @@ -464,7 +436,6 @@ public async Task Record_Created_Recently() ## Working with Date Components - ```csharp [Test] public async Task Date_Components() @@ -484,7 +455,6 @@ public async Task Date_Components() DayOfWeek has its own assertions: - ```csharp [Test] public async Task Day_Of_Week_Checks() @@ -504,7 +474,6 @@ public async Task Day_Of_Week_Checks() ## Chaining DateTime Assertions - ```csharp [Test] public async Task Chained_DateTime_Assertions() @@ -522,7 +491,6 @@ public async Task Chained_DateTime_Assertions() ### Birthday Validation - ```csharp [Test] public async Task Validate_Birthday() @@ -536,7 +504,6 @@ public async Task Validate_Birthday() ### Meeting Scheduler - ```csharp [Test] public async Task Schedule_Meeting() @@ -551,7 +518,6 @@ public async Task Schedule_Meeting() ### Relative Time Checks - ```csharp [Test] public async Task Within_Last_Hour() diff --git a/docs/docs/assertions/dictionaries.md b/docs/docs/assertions/dictionaries.md index 9ee0ed982a4..e4203c11cde 100644 --- a/docs/docs/assertions/dictionaries.md +++ b/docs/docs/assertions/dictionaries.md @@ -2,7 +2,6 @@ sidebar_position: 6.8 --- - # Dictionary Assertions @@ -14,7 +13,6 @@ TUnit provides specialized assertions for testing dictionaries (`IReadOnlyDictio Tests that a dictionary contains a specific key: - ```csharp [Test] public async Task Dictionary_Contains_Key() @@ -33,7 +31,6 @@ public async Task Dictionary_Contains_Key() #### With Custom Comparer - ```csharp [Test] public async Task Contains_Key_With_Comparer() @@ -54,7 +51,6 @@ public async Task Contains_Key_With_Comparer() Tests that a dictionary does not contain a specific key: - ```csharp [Test] public async Task Dictionary_Does_Not_Contain_Key() @@ -76,7 +72,6 @@ public async Task Dictionary_Does_Not_Contain_Key() Tests that a dictionary contains a specific value: - ```csharp [Test] public async Task Dictionary_Contains_Value() @@ -99,7 +94,6 @@ Dictionaries inherit all collection assertions since they implement `IEnumerable ### Count - ```csharp [Test] public async Task Dictionary_Count() @@ -117,7 +111,6 @@ public async Task Dictionary_Count() ### IsEmpty / IsNotEmpty - ```csharp [Test] public async Task Dictionary_Empty() @@ -132,7 +125,6 @@ public async Task Dictionary_Empty() ### Contains (KeyValuePair) - ```csharp [Test] public async Task Dictionary_Contains_Pair() @@ -149,7 +141,6 @@ public async Task Dictionary_Contains_Pair() ### All Pairs Match Condition - ```csharp [Test] public async Task All_Values_Positive() @@ -167,7 +158,6 @@ public async Task All_Values_Positive() ### Any Pair Matches Condition - ```csharp [Test] public async Task Any_Key_Starts_With() @@ -187,12 +177,11 @@ public async Task Any_Key_Starts_With() ### Configuration Validation - ```csharp [Test] public async Task Configuration_Has_Required_Keys() { - var config = LoadConfiguration(); + var config = GetConfigurationValues(); using (Assert.Multiple()) { @@ -205,7 +194,6 @@ public async Task Configuration_Has_Required_Keys() ### HTTP Headers Validation - ```csharp [Test] public async Task Response_Headers() @@ -224,7 +212,6 @@ public async Task Response_Headers() ### Lookup Table Validation - ```csharp [Test] public async Task Lookup_Table() @@ -245,7 +232,6 @@ public async Task Lookup_Table() ### Cache Validation - ```csharp [Test] public async Task Cache_Contains_Entry() @@ -267,7 +253,6 @@ public async Task Cache_Contains_Entry() ### Accessing Values After Key Check - ```csharp [Test] public async Task Get_Value_After_Key_Check() @@ -288,7 +273,6 @@ public async Task Get_Value_After_Key_Check() ### TryGetValue Pattern - ```csharp [Test] public async Task TryGetValue_Pattern() @@ -309,7 +293,6 @@ public async Task TryGetValue_Pattern() ### Keys Collection - ```csharp [Test] public async Task Dictionary_Keys() @@ -333,7 +316,6 @@ public async Task Dictionary_Keys() ### Values Collection - ```csharp [Test] public async Task Dictionary_Values() @@ -359,7 +341,6 @@ public async Task Dictionary_Values() ### Same Key-Value Pairs - ```csharp [Test] public async Task Dictionaries_Are_Equivalent() @@ -383,7 +364,6 @@ public async Task Dictionaries_Are_Equivalent() ## Chaining Dictionary Assertions - ```csharp [Test] public async Task Chained_Dictionary_Assertions() @@ -409,7 +389,6 @@ public async Task Chained_Dictionary_Assertions() ### ConcurrentDictionary - ```csharp [Test] public async Task Concurrent_Dictionary() @@ -426,7 +405,6 @@ public async Task Concurrent_Dictionary() ### ReadOnlyDictionary - ```csharp [Test] public async Task ReadOnly_Dictionary() @@ -442,7 +420,6 @@ public async Task ReadOnly_Dictionary() ### SortedDictionary - ```csharp [Test] public async Task Sorted_Dictionary() @@ -464,7 +441,6 @@ public async Task Sorted_Dictionary() ### Null Dictionary - ```csharp [Test] public async Task Null_Dictionary() @@ -477,7 +453,6 @@ public async Task Null_Dictionary() ### Empty vs Null - ```csharp [Test] public async Task Empty_vs_Null_Dictionary() @@ -495,12 +470,11 @@ public async Task Empty_vs_Null_Dictionary() ### Required Configuration Keys - ```csharp [Test] public async Task All_Required_Keys_Present() { - var config = LoadConfiguration(); + var config = GetConfigurationValues(); var requiredKeys = new[] { "ApiKey", "Database", "Environment" }; foreach (var key in requiredKeys) @@ -512,12 +486,11 @@ public async Task All_Required_Keys_Present() Or with `Assert.Multiple`: - ```csharp [Test] public async Task All_Required_Keys_Present_Multiple() { - var config = LoadConfiguration(); + var config = GetConfigurationValues(); var requiredKeys = new[] { "ApiKey", "Database", "Environment" }; using (Assert.Multiple()) @@ -532,7 +505,6 @@ public async Task All_Required_Keys_Present_Multiple() ### Metadata Validation - ```csharp [Test] public async Task Validate_Metadata() @@ -549,7 +521,6 @@ public async Task Validate_Metadata() ### Feature Flags - ```csharp [Test] public async Task Feature_Flags() diff --git a/docs/docs/assertions/equality-and-comparison.md b/docs/docs/assertions/equality-and-comparison.md index 9025e320700..3ffee49074d 100644 --- a/docs/docs/assertions/equality-and-comparison.md +++ b/docs/docs/assertions/equality-and-comparison.md @@ -2,7 +2,6 @@ sidebar_position: 2 --- - # Equality and Comparison Assertions @@ -14,7 +13,6 @@ TUnit provides comprehensive assertions for testing equality and comparing value Tests that two values are equal using the type's `Equals()` method or `==` operator: - ```csharp [Test] public async Task Basic_Equality() @@ -34,7 +32,6 @@ public async Task Basic_Equality() Tests that two values are not equal: - ```csharp [Test] public async Task Not_Equal() @@ -53,7 +50,6 @@ public async Task Not_Equal() Tests that two references point to the exact same object instance: - ```csharp [Test] public async Task Same_Reference() @@ -69,7 +65,6 @@ public async Task Same_Reference() Tests that two references point to different object instances: - ```csharp [Test] public async Task Different_References() @@ -89,7 +84,6 @@ All comparison assertions work with types that implement `IComparable` or `IC ### IsGreaterThan - ```csharp [Test] public async Task Greater_Than() @@ -107,7 +101,6 @@ public async Task Greater_Than() ### IsGreaterThanOrEqualTo - ```csharp [Test] public async Task Greater_Than_Or_Equal() @@ -122,7 +115,6 @@ public async Task Greater_Than_Or_Equal() ### IsLessThan - ```csharp [Test] public async Task Less_Than() @@ -137,7 +129,6 @@ public async Task Less_Than() ### IsLessThanOrEqualTo - ```csharp [Test] public async Task Less_Than_Or_Equal() @@ -154,7 +145,6 @@ public async Task Less_Than_Or_Equal() Tests that a value falls within a range (inclusive): - ```csharp [Test] public async Task Between_Values() @@ -172,7 +162,6 @@ public async Task Between_Values() Boundary values are included: - ```csharp [Test] public async Task Between_Includes_Boundaries() @@ -189,7 +178,6 @@ public async Task Between_Includes_Boundaries() Tests that a numeric value is greater than zero: - ```csharp [Test] public async Task Positive_Numbers() @@ -214,7 +202,6 @@ public async Task Positive_Numbers() Tests that a numeric value is less than zero: - ```csharp [Test] public async Task Negative_Numbers() @@ -233,7 +220,6 @@ When comparing floating-point numbers, you can specify a tolerance to account fo ### Double Tolerance - ```csharp [Test] public async Task Double_With_Tolerance() @@ -251,7 +237,6 @@ public async Task Double_With_Tolerance() ### Float Tolerance - ```csharp [Test] public async Task Float_With_Tolerance() @@ -265,7 +250,6 @@ public async Task Float_With_Tolerance() ### Decimal Tolerance - ```csharp [Test] public async Task Decimal_With_Tolerance() @@ -279,7 +263,6 @@ public async Task Decimal_With_Tolerance() ### Long Tolerance - ```csharp [Test] public async Task Long_With_Tolerance() @@ -297,7 +280,6 @@ public async Task Long_With_Tolerance() Combine multiple comparison assertions: - ```csharp [Test] public async Task Chained_Comparisons() @@ -313,7 +295,6 @@ public async Task Chained_Comparisons() Or use `IsBetween` for simpler range checks: - ```csharp [Test] public async Task Range_Check_Simplified() @@ -329,7 +310,6 @@ public async Task Range_Check_Simplified() You can provide custom equality comparers for collections and complex types: - ```csharp [Test] public async Task Custom_Comparer() @@ -357,7 +337,6 @@ public class PersonNameComparer : IEqualityComparer Or use a predicate: - ```csharp [Test] public async Task Custom_Equality_Predicate() @@ -367,7 +346,7 @@ public async Task Custom_Equality_Predicate() await Assert.That(people1) .IsEquivalentTo(people2) - .Using((p1, p2) => string.Equals(p1.Name, p2.Name, + .Using((p1, p2) => string.Equals(p1!.Name, p2!.Name, StringComparison.OrdinalIgnoreCase)); } ``` @@ -376,7 +355,6 @@ public async Task Custom_Equality_Predicate() Equality works naturally with value types and records: - ```csharp public record Point(int X, int Y); @@ -392,7 +370,6 @@ public async Task Record_Equality() } ``` - ```csharp public struct Coordinate { diff --git a/docs/docs/assertions/exceptions.md b/docs/docs/assertions/exceptions.md index a1b86baa610..2a949bdf31d 100644 --- a/docs/docs/assertions/exceptions.md +++ b/docs/docs/assertions/exceptions.md @@ -2,7 +2,6 @@ sidebar_position: 8 --- - # Exception Assertions @@ -14,7 +13,6 @@ TUnit provides comprehensive assertions for testing that code throws (or doesn't Tests that a delegate throws a specific exception type (or a subclass): - ```csharp [Test] public async Task Code_Throws_Exception() @@ -26,7 +24,6 @@ public async Task Code_Throws_Exception() Works with any exception type: - ```csharp [Test] public async Task Various_Exception_Types() @@ -46,7 +43,6 @@ public async Task Various_Exception_Types() Tests that a delegate throws the exact exception type (not a subclass): - ```csharp [Test] public async Task Throws_Exact_Type() @@ -64,7 +60,6 @@ public async Task Throws_Exact_Type() Use when the exception type is only known at runtime. On a synchronous delegate, fluent chaining only supports the generic `Throws()` / `ThrowsExactly()` forms, so reach for the static `Assert.Throws(Type, Action)` helper instead. On an async delegate you can call `ThrowsAsync(Type)` directly. - ```csharp [Test] public async Task Throws_Runtime_Type_Sync() @@ -92,7 +87,6 @@ public async Task Throws_Runtime_Type_Async() Tests that code does not throw any exception: - ```csharp [Test] public async Task Code_Does_Not_Throw() @@ -109,7 +103,6 @@ public async Task Code_Does_Not_Throw() For async operations, use async delegates: - ```csharp [Test] public async Task Async_Throws_Exception() @@ -119,7 +112,6 @@ public async Task Async_Throws_Exception() } ``` - ```csharp [Test] public async Task Async_Does_Not_Throw() @@ -135,7 +127,6 @@ public async Task Async_Does_Not_Throw() Tests that the exception has an exact message: - ```csharp [Test] public async Task Exception_With_Exact_Message() @@ -150,7 +141,6 @@ public async Task Exception_With_Exact_Message() Tests that the exception message contains a substring: - ```csharp [Test] public async Task Exception_Message_Contains() @@ -163,15 +153,13 @@ public async Task Exception_Message_Contains() #### Case-Insensitive - ```csharp [Test] public async Task Message_Contains_Ignoring_Case() { await Assert.That(() => throw new Exception("ERROR: Failed")) .Throws() - .WithMessageContaining("error") - .IgnoringCase(); + .WithMessageContaining("error", StringComparison.OrdinalIgnoreCase); } ``` @@ -179,7 +167,6 @@ public async Task Message_Contains_Ignoring_Case() Tests that the exception message does not contain a substring: - ```csharp [Test] public async Task Message_Does_Not_Contain() @@ -194,7 +181,6 @@ public async Task Message_Does_Not_Contain() Tests that the exception message matches a pattern: - ```csharp [Test] public async Task Message_Matches_Pattern() @@ -207,12 +193,11 @@ public async Task Message_Matches_Pattern() Or with a `StringMatcher`: - ```csharp [Test] public async Task Message_Matches_With_Matcher() { - var matcher = new StringMatcher("Error * occurred", caseSensitive: false); + var matcher = StringMatcher.AsWildcard("Error * occurred").IgnoringCase(); await Assert.That(() => throw new Exception("Error 500 occurred")) .Throws() @@ -226,7 +211,6 @@ public async Task Message_Matches_With_Matcher() For `ArgumentException` and its subclasses, you can assert on the parameter name: - ```csharp [Test] public async Task ArgumentException_With_Parameter_Name() @@ -245,15 +229,14 @@ void ValidateUser(User user) Combine with message assertions: - ```csharp [Test] public async Task ArgumentException_Parameter_And_Message() { - await Assert.That(() => SetAge(-1)) - .Throws() - .WithParameterName("age") - .WithMessageContaining("must be positive"); + var exception = await Assert.That(() => SetAge(-1)) + .Throws(); + await Assert.That(exception!.ParamName).IsEqualTo("age"); + await Assert.That(exception.Message).Contains("must be positive"); } void SetAge(int age) @@ -269,7 +252,6 @@ void SetAge(int age) Assert on the inner exception: - ```csharp [Test] public async Task Exception_With_Inner_Exception() @@ -291,15 +273,13 @@ public async Task Exception_With_Inner_Exception() Chain to assert on the inner exception type: - ```csharp [Test] public async Task Inner_Exception_Type() { await Assert.That(() => ThrowWithInner()) .Throws() - .WithInnerException() - .Throws(); + .WithInnerException(); } void ThrowWithInner() @@ -319,26 +299,24 @@ void ThrowWithInner() ### Validation Exceptions - ```csharp [Test] public async Task Validate_Email_Throws() { - await Assert.That(() => ValidateEmail("invalid-email")) - .Throws() - .WithParameterName("email") - .WithMessageContaining("valid email"); + var exception = await Assert.That(() => ValidateEmail("invalid-email")) + .Throws(); + await Assert.That(exception!.ParamName).IsEqualTo("email"); + await Assert.That(exception.Message).Contains("valid email"); } ``` ### Null Argument Checks - ```csharp [Test] public async Task Null_Argument_Throws() { - await Assert.That(() => ProcessData(null!)) + await Assert.That(() => ProcessData((object?) null)) .Throws() .WithParameterName("data"); } @@ -346,7 +324,6 @@ public async Task Null_Argument_Throws() ### File Operations - ```csharp [Test] public async Task File_Not_Found() @@ -359,7 +336,6 @@ public async Task File_Not_Found() ### Network Operations - ```csharp [Test] public async Task HTTP_Request_Fails() @@ -371,7 +347,6 @@ public async Task HTTP_Request_Fails() ### Database Operations - ```csharp [Test] public async Task Duplicate_Key_Violation() @@ -384,7 +359,6 @@ public async Task Duplicate_Key_Violation() ### Division by Zero - ```csharp [Test] public async Task Division_By_Zero() @@ -400,7 +374,6 @@ public async Task Division_By_Zero() ### Index Out of Range - ```csharp [Test] public async Task Array_Index_Out_Of_Range() @@ -414,7 +387,6 @@ public async Task Array_Index_Out_Of_Range() ### Invalid Cast - ```csharp [Test] public async Task Invalid_Cast() @@ -428,7 +400,6 @@ public async Task Invalid_Cast() ### Custom Exceptions - ```csharp public class BusinessRuleException : Exception { @@ -449,7 +420,7 @@ public async Task Custom_Exception_With_Properties() .Throws(); // Can't directly assert on exception properties yet, but you can access them - await Assert.That(exception.RuleCode).IsEqualTo("BR001"); + await Assert.That(exception!.RuleCode).IsEqualTo("BR001"); await Assert.That(exception.Message).Contains("Business rule"); } ``` @@ -458,7 +429,6 @@ public async Task Custom_Exception_With_Properties() ### Using Assert.Multiple - ```csharp [Test] public async Task Multiple_Exception_Scenarios() @@ -481,7 +451,6 @@ public async Task Multiple_Exception_Scenarios() When using `Throws()`, subclasses are accepted: - ```csharp [Test] public async Task Exception_Inheritance() @@ -497,7 +466,6 @@ public async Task Exception_Inheritance() Use `ThrowsExactly()` if you need the exact type: - ```csharp [Test] public async Task Exact_Exception_Type() @@ -513,7 +481,6 @@ public async Task Exact_Exception_Type() ## Aggregate Exceptions - ```csharp [Test] public async Task Aggregate_Exception() @@ -529,16 +496,15 @@ public async Task Aggregate_Exception() ## Chaining Exception Assertions - ```csharp [Test] public async Task Chained_Exception_Assertions() { - await Assert.That(() => ValidateInput("")) - .Throws() - .WithParameterName("input") - .WithMessageContaining("cannot be empty") - .WithMessageNotContaining("null"); + var exception = await Assert.That(() => ValidateInput("")) + .Throws(); + await Assert.That(exception!.ParamName).IsEqualTo("input"); + await Assert.That(exception.Message).Contains("cannot be empty"); + await Assert.That(exception.Message).DoesNotContain("null"); } ``` @@ -546,7 +512,6 @@ public async Task Chained_Exception_Assertions() ### ThrowsNothing vs Try-Catch - ```csharp [Test] public async Task Explicit_No_Exception() @@ -564,7 +529,6 @@ public async Task Explicit_No_Exception() ### Expected Failures - ```csharp [Test] public async Task Expected_Validation_Failure() @@ -579,7 +543,6 @@ public async Task Expected_Validation_Failure() ### Defensive Programming - ```csharp [Test] public async Task Guard_Clause_Validation() @@ -592,7 +555,6 @@ public async Task Guard_Clause_Validation() ### State Validation - ```csharp [Test] public async Task Invalid_State_Operation() @@ -608,7 +570,6 @@ public async Task Invalid_State_Operation() ### Configuration Errors - ```csharp [Test] public async Task Missing_Configuration() @@ -621,7 +582,6 @@ public async Task Missing_Configuration() ## Timeout Exceptions - ```csharp [Test] public async Task Operation_Timeout() @@ -635,7 +595,6 @@ public async Task Operation_Timeout() ## Re-throwing Exceptions - ```csharp [Test] public async Task Wrapper_Exception() @@ -657,7 +616,6 @@ public async Task Wrapper_Exception() ## Exception Assertions with Async/Await - ```csharp [Test] public async Task Async_Exception_Handling() diff --git a/docs/docs/assertions/extensibility/custom-assertions.md b/docs/docs/assertions/extensibility/custom-assertions.md index 912397912ad..1e47f60dd5e 100644 --- a/docs/docs/assertions/extensibility/custom-assertions.md +++ b/docs/docs/assertions/extensibility/custom-assertions.md @@ -2,7 +2,6 @@ sidebar_position: 1 --- - # Custom Assertions @@ -110,3 +109,4 @@ await Assert.That("Hello World") - **Context sharing**: Pass `source.Context` to your assertion constructor (it contains the evaluation context and expression builder) - **CheckAsync parameter**: Use `EvaluationMetadata metadata` which contains both `Value` and `Exception` properties - **CallerArgumentExpression**: Use this attribute to capture parameter expressions for better error messages + diff --git a/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md b/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md index e92cf8bb465..ddc6a1cb7b6 100644 --- a/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md +++ b/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md @@ -2,7 +2,6 @@ sidebar_position: 3 --- - # Chaining and Converting @@ -13,16 +12,22 @@ Chaining is especially helpful when you want to perform multiple assertions on a For example: - ```csharp - HttpResponseMessage response = ...; - - await Assert.That(response) - .IsProblemDetails() - .And - .HasTitle("Invalid Authentication Token") - .And - .HasDetail("No token provided"); +using var response = new HttpResponseMessage(HttpStatusCode.BadRequest) +{ + Content = JsonContent.Create(new ProblemDetails + { + Title = "Invalid Authentication Token", + Detail = "No token provided" + }) +}; + +await Assert.That(response) + .IsProblemDetails() + .And + .HasTitle("Invalid Authentication Token") + .And + .HasDetail("No token provided"); ``` The `response` object initially passed in is a `HttpResponseMessage`, but then after we assert it's a `ProblemDetails` object, the chain has changed to that type so that we can further assert with methods specific to `ProblemDetails` instead of `HttpResponseMessage`. @@ -43,7 +48,7 @@ public class IsProblemDetailsAssertion : Assertion public IsProblemDetailsAssertion(AssertionContext context) : base(context.Map(async response => { - var content = await response.Content.ReadFromJsonAsync(); + var content = await response!.Content.ReadFromJsonAsync(); if (content is null) { @@ -193,3 +198,4 @@ TUnit includes several built-in examples of type conversion assertions: - `WhenParsedInto()` - Converts a string to a parsed type (e.g., `await Assert.That("123").WhenParsedInto().IsEqualTo(123)`) - `IsTypeOf()` - Converts to a specific type (e.g., `await Assert.That(obj).IsTypeOf().Length().IsEqualTo(5)`) + 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 64649277267..a426f9754f5 100644 --- a/docs/docs/assertions/extensibility/extensibility-returning-items-from-await.md +++ b/docs/docs/assertions/extensibility/extensibility-returning-items-from-await.md @@ -2,7 +2,6 @@ sidebar_position: 4 --- - # Returning Data via `await` @@ -101,7 +100,7 @@ You can now use the assertion and get the found item: ```csharp // Returns the first item with price < 0.99 -Product cheapProduct = await Assert.That(products).Contains(p => p.Price < 0.99); +Product cheapProduct = await Assert.That(products).Contains(p => p.Price < 0.99m); // Use the returned value in further assertions await Assert.That(cheapProduct.Name).IsNotNull(); @@ -115,3 +114,4 @@ TUnit includes several built-in examples of assertions that return values: - `Contains(predicate)` - Returns the first item matching the predicate - `WhenParsedInto()` - Returns the parsed value (e.g., `int value = await Assert.That("123").WhenParsedInto()`) - `IsTypeOf()` - Returns the casted value (e.g., `StringBuilder sb = await Assert.That(obj).IsTypeOf()`) + diff --git a/docs/docs/assertions/extensibility/source-generator-assertions.md b/docs/docs/assertions/extensibility/source-generator-assertions.md index 99850dedbdd..6cb1658dc8b 100644 --- a/docs/docs/assertions/extensibility/source-generator-assertions.md +++ b/docs/docs/assertions/extensibility/source-generator-assertions.md @@ -2,7 +2,6 @@ sidebar_position: 2 --- - # Source Generator Assertions @@ -52,7 +51,7 @@ The generator creates: 2. An extension method on `IAssertionSource` 3. Full support for chaining with `.And` and `.Or` -```csharp +```text // Generated code (simplified): public sealed class IsPositive_Assertion : Assertion { @@ -116,14 +115,14 @@ public static bool IsBetween(this int value, int min, int max) ```csharp [EditorBrowsable(EditorBrowsableState.Never)] [GenerateAssertion(ExpectationMessage = "to be even")] -public static bool IsEven(this int value) +public static bool HasEvenValue(this int value) { return value % 2 == 0; } // Usage: -await Assert.That(4).IsEven(); // ✅ Passes -await Assert.That(3).IsEven(); // ❌ Fails: "Expected to be even but found 3" +await Assert.That(4).HasEvenValue(); // ✅ Passes +await Assert.That(3).HasEvenValue(); // ❌ Fails: "Expected to be even but found 3" ``` ### 2. `AssertionResult` - Custom Messages @@ -157,12 +156,14 @@ await Assert.That(15).IsPrime(); // ❌ Fails: "Expected to be prime but 15 is ```csharp [EditorBrowsable(EditorBrowsableState.Never)] [GenerateAssertion(ExpectationMessage = "to exist in database")] -public static async Task ExistsInDatabaseAsync(this int userId, DbContext db) +public static async Task ExistsInDatabaseAsync(this int userId, ApplicationDbContext db) { - return await db.Users.AnyAsync(u => u.Id == userId); + return await db.Users.AnyAsync(u => Equals(u.Id, userId)); } // Usage: +var userId = 123; +await using var dbContext = new ApplicationDbContext(); await Assert.That(userId).ExistsInDatabaseAsync(dbContext); // If fails: "Expected to exist in database but found 123" ``` @@ -172,7 +173,7 @@ await Assert.That(userId).ExistsInDatabaseAsync(dbContext); ```csharp [EditorBrowsable(EditorBrowsableState.Never)] [GenerateAssertion(ExpectationMessage = "to have valid email")] -public static async Task HasValidEmailAsync(this int userId, DbContext db) +public static async Task HasValidEmailAsync(this int userId, ApplicationDbContext db) { var user = await db.Users.FindAsync(userId); @@ -186,6 +187,7 @@ public static async Task HasValidEmailAsync(this int userId, Db } // Usage: +await using var dbContext = new ApplicationDbContext(); await Assert.That(123).HasValidEmailAsync(dbContext); // If fails: "Expected to have valid email but User 123 not found" ``` @@ -236,18 +238,18 @@ Use `[AssertionFrom]` to create assertions from existing methods in libraries or ```csharp using TUnit.Assertions.Attributes; -[AssertionFrom(nameof(string.IsNullOrEmpty), ExpectationMessage = "to be null or empty")] -[AssertionFrom(nameof(string.StartsWith), ExpectationMessage = "to start with {value}")] -[AssertionFrom(nameof(string.EndsWith), ExpectationMessage = "to end with {value}")] +[AssertionFrom(nameof(string.IsNullOrEmpty), CustomName = "IsBlank", ExpectationMessage = "to be null or empty")] +[AssertionFrom(nameof(string.StartsWith), CustomName = "BeginsWithText", ExpectationMessage = "to start with {value}")] +[AssertionFrom(nameof(string.EndsWith), CustomName = "EndsWithText", ExpectationMessage = "to end with {value}")] public static partial class StringAssertionExtensions { } // Usage: -await Assert.That(myString).IsNullOrEmpty(); +await Assert.That("").IsBlank(); // If fails: "Expected to be null or empty but found 'test'" -await Assert.That("hello").StartsWith("he"); +await Assert.That("hello").BeginsWithText("he"); // If fails: "Expected to start with 'he' but found 'hello'" ``` @@ -269,14 +271,14 @@ await Assert.That("hello").Has("world"); // ❌ Fails: "Expected to have For `bool`-returning methods, you can generate negated versions: ```csharp -[AssertionFrom(nameof(string.Contains), CustomName = "DoesNotContain", NegateLogic = true, ExpectationMessage = "to not contain '{value}'")] +[AssertionFrom(nameof(string.Contains), CustomName = "LacksText", NegateLogic = true, ExpectationMessage = "to not contain '{value}'")] public static partial class StringAssertionExtensions { } // Usage: -await Assert.That("hello").DoesNotContain("xyz"); // ✅ Passes -await Assert.That("hello").DoesNotContain("ell"); // ❌ Fails: "Expected to not contain 'ell' but found 'hello'" +await Assert.That("hello").LacksText("xyz"); // ✅ Passes +await Assert.That("hello").LacksText("ell"); // ❌ Fails: "Expected to not contain 'ell' but found 'hello'" ``` **Note:** Negation only works with `bool`-returning methods. `AssertionResult` methods determine their own pass/fail logic. @@ -321,21 +323,22 @@ using TUnit.Assertions.Attributes; file static class BoolAssertions { [GenerateAssertion(ExpectationMessage = "to be true", InlineMethodBody = true)] - public static bool IsTrue(this bool value) => value == true; + public static bool HasTrueValue(this bool value) => value == true; [GenerateAssertion(ExpectationMessage = "to be false", InlineMethodBody = true)] - public static bool IsFalse(this bool value) => value == false; + public static bool HasFalseValue(this bool value) => value == false; } // Usage in tests: -await Assert.That(myBool).IsTrue(); // ✅ Clean API, no IntelliSense pollution +var myBool = true; +await Assert.That(myBool).HasTrueValue(); // ✅ Clean API, no IntelliSense pollution ``` ### What Gets Generated with Inlining Instead of calling your method, the generator inlines the expression directly: -```csharp +```text // WITHOUT InlineMethodBody (calls the method): protected override Task CheckAsync(EvaluationMetadata metadata) { @@ -417,7 +420,7 @@ public static partial class StringAssertionExtensions // ❌ BAD: Will appear in IntelliSense when typing on string values [GenerateAssertion] - public static bool IsEmptyString(this string value) => value.Length == 0; + public static bool IsEmptyStringVisible(this string value) => value.Length == 0; } ``` @@ -455,16 +458,17 @@ public static bool IsPositive(this int value) => value > 0; [EditorBrowsable(EditorBrowsableState.Never)] [GenerateAssertion(ExpectationMessage = "to be even")] -public static bool IsEven(this int value) => value % 2 == 0; +public static bool HasEvenValue(this int value) => value % 2 == 0; // Usage: await Assert.That(10) .IsPositive() - .And.IsEven(); + .And.HasEvenValue(); // Or: +var number = 2; await Assert.That(number) - .IsEven() + .HasEvenValue() .Or.IsPositive(); ``` @@ -486,7 +490,7 @@ public static partial class UserAssertionExtensions [GenerateAssertion(ExpectationMessage = "to have valid ID")] public static bool HasValidId(this User user) { - return user.Id > 0; + return user.Id is int id && id > 0; } // With parameters @@ -514,9 +518,9 @@ public static partial class UserAssertionExtensions // Async with database [EditorBrowsable(EditorBrowsableState.Never)] [GenerateAssertion(ExpectationMessage = "to exist in database")] - public static async Task ExistsInDatabaseAsync(this User user, DbContext db) + public static async Task ExistsInDatabaseAsync(this User user, ApplicationDbContext db) { - return await db.Users.AnyAsync(u => u.Id == user.Id); + return await db.Users.AnyAsync(u => Equals(u.Id, user.Id)); } } @@ -525,6 +529,7 @@ public static partial class UserAssertionExtensions public async Task ValidateUser() { var user = new User { Id = 1, Email = "test@example.com", Roles = ["Admin"] }; + await using var dbContext = new ApplicationDbContext(); await Assert.That(user).HasValidId(); await Assert.That(user).HasRole("Admin"); diff --git a/docs/docs/assertions/getting-started.md b/docs/docs/assertions/getting-started.md index 05707d8fa94..ade135f010a 100644 --- a/docs/docs/assertions/getting-started.md +++ b/docs/docs/assertions/getting-started.md @@ -2,7 +2,6 @@ sidebar_position: 1 --- - # Getting Started with Assertions @@ -12,7 +11,6 @@ TUnit provides a comprehensive, fluent assertion library that makes your tests r All assertions in TUnit follow a consistent pattern using the `Assert.That()` method: - ```csharp await Assert.That(actualValue).IsEqualTo(expectedValue); ``` @@ -26,8 +24,7 @@ The basic flow is: TUnit assertions must be awaited — they won't execute without `await`, and the test will pass silently: - -```csharp +```text // ✅ Correct - awaited await Assert.That(result).IsEqualTo(42); @@ -45,7 +42,6 @@ TUnit provides assertions for all common scenarios: ### Equality & Comparison - ```csharp await Assert.That(actual).IsEqualTo(expected); await Assert.That(value).IsNotEqualTo(other); @@ -56,7 +52,6 @@ await Assert.That(temperature).IsBetween(20, 30); ### Strings - ```csharp await Assert.That(message).Contains("Hello"); await Assert.That(filename).StartsWith("test_"); @@ -66,7 +61,6 @@ await Assert.That(input).IsNotEmpty(); ### Collections - ```csharp await Assert.That(numbers).Contains(42); await Assert.That(items).Count().IsEqualTo(5); @@ -76,16 +70,14 @@ await Assert.That(values).All(x => x > 0); ### Booleans & Null - ```csharp await Assert.That(isValid).IsTrue(); -await Assert.That(result).IsNotNull(); +await Assert.That(obj).IsNotNull(); await Assert.That(optional).IsDefault(); ``` ### Exceptions - ```csharp await Assert.That(() => DivideByZero()) .Throws() @@ -94,7 +86,6 @@ await Assert.That(() => DivideByZero()) ### Type Checking - ```csharp await Assert.That(obj).IsTypeOf(); await Assert.That(typeof(Dog)).IsAssignableTo(); @@ -104,18 +95,14 @@ await Assert.That(typeof(Dog)).IsAssignableTo(); Combine multiple assertions on the same value using `.And`: - ```csharp -await Assert.That(username) - .IsNotNull() - .And.IsNotEmpty() - .And.Length().IsGreaterThan(3) - .And.Length().IsLessThan(20); +await Assert.That(username).IsNotNull().And.IsNotEmpty(); +await Assert.That(username).Length().IsGreaterThan(3); +await Assert.That(username).Length().IsLessThan(20); ``` Use `.Or` when any condition can be true: - ```csharp await Assert.That(statusCode) .IsEqualTo(200) @@ -127,7 +114,6 @@ await Assert.That(statusCode) Group related assertions together so all failures are reported: - ```csharp using (Assert.Multiple()) { @@ -144,7 +130,6 @@ Instead of stopping at the first failure, `Assert.Multiple()` runs all assertion Assert on object properties using `.Member()`: - ```csharp await Assert.That(person) .Member(p => p.Name, name => name.IsEqualTo("Alice")) @@ -153,7 +138,6 @@ await Assert.That(person) This works with nested properties too: - ```csharp await Assert.That(order) .Member(o => o.Customer.Address.City, city => city.IsEqualTo("Seattle")); @@ -163,7 +147,6 @@ await Assert.That(order) Collections have rich assertion support: - ```csharp var numbers = new[] { 1, 2, 3, 4, 5 }; @@ -190,7 +173,6 @@ await Assert.That(numbers).IsEquivalentTo(new[] { 5, 4, 3, 2, 1 }); Some assertions return the value being tested, allowing you to continue working with it: - ```csharp // HasSingleItem returns the single item var user = await Assert.That(users).HasSingleItem(); @@ -205,24 +187,21 @@ await Assert.That(admin.Permissions).IsNotEmpty(); Use `.Satisfies()` for custom conditions: - ```csharp await Assert.That(value).Satisfies(v => v % 2 == 0, "Value must be even"); ``` Or map to a different value before asserting: - ```csharp await Assert.That(order) - .Satisfies(o => o.Total, total => total > 100); + .Member(o => o.Total, total => total.IsGreaterThan(100)); ``` ## Common Patterns ### Testing Numeric Ranges - ```csharp await Assert.That(score).IsBetween(0, 100); await Assert.That(temperature).IsGreaterThanOrEqualTo(32); @@ -232,14 +211,12 @@ await Assert.That(temperature).IsGreaterThanOrEqualTo(32); For floating-point comparisons: - ```csharp await Assert.That(3.14159).IsEqualTo(Math.PI).Within(0.001); ``` ### Testing Async Operations - ```csharp await Assert.That(async () => await FetchDataAsync()) .Throws(); @@ -249,11 +226,10 @@ await Assert.That(longRunningTask).CompletesWithin(TimeSpan.FromSeconds(5)); ### Testing Multiple Conditions - ```csharp await Assert.That(username) .IsNotNull() - .And.Satisfies(name => name.Length >= 3 && name.Length <= 20, + .And.Satisfies(name => name!.Length >= 3 && name.Length <= 20, "Username must be 3-20 characters"); ``` @@ -261,10 +237,10 @@ await Assert.That(username) TUnit's assertions are strongly typed and catch type mismatches at compile time: - ```csharp int number = 42; string text = "42"; +_ = text; // ✅ This works - both are ints await Assert.That(number).IsEqualTo(42); diff --git a/docs/docs/assertions/member-assertions.md b/docs/docs/assertions/member-assertions.md index 507ec324a99..9223522e7b2 100644 --- a/docs/docs/assertions/member-assertions.md +++ b/docs/docs/assertions/member-assertions.md @@ -2,7 +2,6 @@ sidebar_position: 12 --- - # Member Assertions @@ -10,7 +9,6 @@ The `.Member()` method allows you to assert on object properties while maintaini ## Basic Usage - ```csharp [Test] public async Task BasicMemberAssertions() @@ -33,7 +31,6 @@ public async Task BasicMemberAssertions() The key advantage of `.Member()` is that it returns to the parent context after each assertion, allowing you to chain multiple property checks: - ```csharp [Test] public async Task MemberAssertionsWithFullContext() @@ -53,7 +50,6 @@ public async Task MemberAssertionsWithFullContext() Member assertions support nested properties: - ```csharp [Test] public async Task NestedPropertyAssertions() @@ -72,7 +68,6 @@ public async Task NestedPropertyAssertions() You can perform complex assertions on member values, including collections: - ```csharp [Test] public async Task ComplexMemberAssertions() @@ -94,7 +89,6 @@ public async Task ComplexMemberAssertions() Member assertions work with both `.And` and `.Or` combinators: - ```csharp [Test] public async Task MemberAssertionsWithOrLogic() @@ -116,7 +110,6 @@ public async Task MemberAssertionsWithOrLogic() ## Complete Example - ```csharp [Test] public async Task ComplexObjectValidation() @@ -134,7 +127,6 @@ public async Task ComplexObjectValidation() ## Nested Object Assertions - ```csharp [Test] public async Task NestedObjectAssertions() @@ -145,9 +137,9 @@ public async Task NestedObjectAssertions() .IsNotNull() .And.Member(c => c.Name, name => name.IsEqualTo("TechCorp")) .And.Member(c => c.Address.City, city => city.IsEqualTo("Seattle")) - .And.Member(c => c.Address.ZipCode, zip => zip.Matches(@"^\d{5}$")) - .And.Member(c => c.Employees, employees => employees - .Count().IsBetween(100, 500) - .And.All(e => e.Email.EndsWith("@techcorp.com"))); + .And.Member(c => c.Address.ZipCode, zip => zip.Matches(@"^\d{5}$")); + + await Assert.That(company.Employees.Length).IsBetween(1, 500); + await Assert.That(company.Employees).All(e => e.Email.EndsWith("@example.com")); } ``` diff --git a/docs/docs/assertions/null-and-default.md b/docs/docs/assertions/null-and-default.md index 0d72deea596..6fca6380a99 100644 --- a/docs/docs/assertions/null-and-default.md +++ b/docs/docs/assertions/null-and-default.md @@ -2,7 +2,6 @@ sidebar_position: 2.5 --- - # Null and Default Value Assertions @@ -14,12 +13,11 @@ TUnit provides assertions for testing null values and default values. These asse Tests that a value is `null`: - ```csharp [Test] public async Task Null_Value() { - string? result = GetOptionalValue(); + string? result = GetOptionalString(); await Assert.That(result).IsNull(); Person? person = FindPerson("unknown-id"); @@ -31,7 +29,6 @@ public async Task Null_Value() Tests that a value is not `null`: - ```csharp [Test] public async Task Not_Null_Value() @@ -48,7 +45,6 @@ public async Task Not_Null_Value() When you use `IsNotNull()`, C#'s nullability analysis understands that the value is non-null afterward: - ```csharp [Test] public async Task Nullability_Flow() @@ -65,7 +61,6 @@ public async Task Nullability_Flow() This works with chaining too: - ```csharp [Test] public async Task Chained_After_Null_Check() @@ -85,7 +80,6 @@ public async Task Chained_After_Null_Check() Tests that a value equals the default value for its type: - ```csharp [Test] public async Task Default_Values() @@ -113,7 +107,6 @@ public async Task Default_Values() Tests that a value is not the default value for its type: - ```csharp [Test] public async Task Not_Default_Values() @@ -138,7 +131,6 @@ public async Task Not_Default_Values() For reference types, default equals `null`: - ```csharp [Test] public async Task Reference_Type_Defaults() @@ -157,7 +149,6 @@ public async Task Reference_Type_Defaults() For value types, default is the zero-initialized value: - ```csharp [Test] public async Task Value_Type_Defaults() @@ -187,7 +178,6 @@ public async Task Value_Type_Defaults() Nullable value types (`T?`) are reference types, so their default is `null`: - ```csharp [Test] public async Task Nullable_Value_Type_Defaults() @@ -206,7 +196,6 @@ public async Task Nullable_Value_Type_Defaults() ### Optional Parameters and Returns - ```csharp [Test] public async Task Optional_Return_Value() @@ -223,7 +212,6 @@ public async Task Optional_Return_Value() ### Initialization Checks - ```csharp [Test] public async Task Uninitialized_Field() @@ -242,7 +230,6 @@ public async Task Uninitialized_Field() ### Dependency Injection Validation - ```csharp [Test] public async Task Constructor_Injection() @@ -257,7 +244,6 @@ public async Task Constructor_Injection() ### Lazy Initialization - ```csharp [Test] public async Task Lazy_Property() @@ -278,7 +264,6 @@ public async Task Lazy_Property() Use `Assert.Multiple()` to check multiple null conditions: - ```csharp [Test] public async Task Validate_All_Required_Fields() @@ -298,7 +283,6 @@ public async Task Validate_All_Required_Fields() Or chain them: - ```csharp [Test] public async Task Required_Fields_With_Chaining() @@ -307,8 +291,8 @@ public async Task Required_Fields_With_Chaining() await Assert.That(config.DatabaseConnection) .IsNotNull() - .And.Member(c => c.Server).IsNotNull() - .And.Member(c => c.Database).IsNotNull(); + .And.Member(c => c.Server, server => server.IsNotNull()) + .And.Member(c => c.Database, database => database.IsNotNull()); } ``` @@ -316,7 +300,6 @@ public async Task Required_Fields_With_Chaining() ### Structs - ```csharp public struct Rectangle { @@ -337,7 +320,6 @@ public async Task Struct_Default() ### Records - ```csharp public record Person(string Name, int Age); @@ -365,7 +347,6 @@ public async Task Record_Struct_Default() ### Empty Collections vs Null - ```csharp [Test] public async Task Empty_vs_Null() @@ -381,7 +362,6 @@ public async Task Empty_vs_Null() ### Empty Strings vs Null - ```csharp [Test] public async Task Empty_String_vs_Null() @@ -397,7 +377,6 @@ public async Task Empty_String_vs_Null() ### Default GUID - ```csharp [Test] public async Task GUID_Default() @@ -412,7 +391,6 @@ public async Task GUID_Default() ### Default DateTime - ```csharp [Test] public async Task DateTime_Default() @@ -428,7 +406,6 @@ public async Task DateTime_Default() ### Validate Required Dependencies - ```csharp [Test] public async Task All_Dependencies_Provided() @@ -443,7 +420,6 @@ public async Task All_Dependencies_Provided() ### Validate Optional Features - ```csharp [Test] public async Task Optional_Feature_Not_Enabled() @@ -459,7 +435,6 @@ public async Task Optional_Feature_Not_Enabled() ### State Machine Validation - ```csharp [Test] public async Task State_Transitions() diff --git a/docs/docs/assertions/numeric.md b/docs/docs/assertions/numeric.md index 42fbdc30c63..3860a10a422 100644 --- a/docs/docs/assertions/numeric.md +++ b/docs/docs/assertions/numeric.md @@ -2,7 +2,6 @@ sidebar_position: 4.5 --- - # Numeric Assertions @@ -14,7 +13,6 @@ TUnit provides comprehensive assertions for testing numeric values, including sp Tests that a numeric value is greater than zero: - ```csharp [Test] public async Task Positive_Values() @@ -32,7 +30,6 @@ public async Task Positive_Values() Works with all numeric types: - ```csharp [Test] public async Task All_Numeric_Types() @@ -54,7 +51,6 @@ public async Task All_Numeric_Types() Tests that a numeric value is less than zero: - ```csharp [Test] public async Task Negative_Values() @@ -72,7 +68,6 @@ public async Task Negative_Values() ### Zero is Neither Positive Nor Negative - ```csharp [Test] public async Task Zero_Checks() @@ -94,7 +89,6 @@ All comparison operators work with numeric types. See [Equality and Comparison]( ### Quick Reference - ```csharp [Test] public async Task Numeric_Comparisons() @@ -115,7 +109,6 @@ Floating-point arithmetic can introduce rounding errors. Use tolerance for safe ### Double Tolerance - ```csharp [Test] public async Task Double_Tolerance() @@ -133,7 +126,6 @@ public async Task Double_Tolerance() ### Float Tolerance - ```csharp [Test] public async Task Float_Tolerance() @@ -149,7 +141,6 @@ public async Task Float_Tolerance() Useful for monetary calculations: - ```csharp [Test] public async Task Decimal_Tolerance() @@ -165,7 +156,6 @@ public async Task Decimal_Tolerance() For timestamp or large number comparisons: - ```csharp [Test] public async Task Long_Tolerance() @@ -183,7 +173,6 @@ public async Task Long_Tolerance() ### Financial Calculations - ```csharp [Test] public async Task Calculate_Total_Price() @@ -203,7 +192,6 @@ public async Task Calculate_Total_Price() ### Temperature Conversions - ```csharp [Test] public async Task Celsius_To_Fahrenheit() @@ -218,7 +206,6 @@ public async Task Celsius_To_Fahrenheit() ### Percentage Calculations - ```csharp [Test] public async Task Calculate_Percentage() @@ -235,7 +222,6 @@ public async Task Calculate_Percentage() ### Statistical Calculations - ```csharp [Test] public async Task Calculate_Average() @@ -253,7 +239,6 @@ public async Task Calculate_Average() ### Valid Range Checks - ```csharp [Test] public async Task Validate_Age() @@ -267,7 +252,6 @@ public async Task Validate_Age() ### Percentage Range - ```csharp [Test] public async Task Validate_Percentage() @@ -281,7 +265,6 @@ public async Task Validate_Percentage() ### Score Validation - ```csharp [Test] public async Task Validate_Score() @@ -297,7 +280,6 @@ public async Task Validate_Score() ## Chaining Numeric Assertions - ```csharp [Test] public async Task Chained_Numeric_Assertions() @@ -314,7 +296,6 @@ public async Task Chained_Numeric_Assertions() ## Nullable Numeric Types - ```csharp [Test] public async Task Nullable_Numerics() @@ -339,7 +320,6 @@ public async Task Nullable_Null() ### Infinity - ```csharp [Test] public async Task Infinity_Checks() @@ -354,7 +334,6 @@ public async Task Infinity_Checks() ### NaN (Not a Number) - ```csharp [Test] public async Task NaN_Checks() @@ -371,7 +350,6 @@ public async Task NaN_Checks() ## Performance Metrics - ```csharp [Test] public async Task Response_Time_Check() @@ -391,7 +369,6 @@ public async Task Response_Time_Check() ### Boundary Testing - ```csharp [Test] public async Task Boundary_Values() @@ -407,7 +384,6 @@ public async Task Boundary_Values() ### Growth Rate Validation - ```csharp [Test] public async Task Growth_Rate() @@ -423,7 +399,6 @@ public async Task Growth_Rate() ### Ratio Calculations - ```csharp [Test] public async Task Success_Ratio() diff --git a/docs/docs/assertions/regex-assertions.md b/docs/docs/assertions/regex-assertions.md index b834d5c1ac2..b57bb453786 100644 --- a/docs/docs/assertions/regex-assertions.md +++ b/docs/docs/assertions/regex-assertions.md @@ -2,7 +2,6 @@ sidebar_position: 13 --- - # Regex Assertions @@ -40,7 +39,6 @@ The key advantage of regex assertions is the ability to assert on capture groups ### Named Groups - ```csharp [Test] public async Task NamedGroupAssertions() @@ -58,7 +56,6 @@ public async Task NamedGroupAssertions() ### Indexed Groups - ```csharp [Test] public async Task IndexedGroupAssertions() @@ -80,7 +77,6 @@ public async Task IndexedGroupAssertions() When a regex matches multiple times in a string, you can access specific matches using `.Match(index)`: - ```csharp [Test] public async Task MultipleMatchAssertions() @@ -105,7 +101,6 @@ public async Task MultipleMatchAssertions() To assert on where a match occurs or how long it is, use `.Match(index)` to select a match from the collection, then assert on the resulting `RegexMatch` (you can also combine this with `Regex.Match(...)` directly if you need more detailed inspection): - ```csharp [Test] public async Task PositionAndLengthAssertions() @@ -128,7 +123,6 @@ public async Task PositionAndLengthAssertions() ## Complex Patterns with Multiple Groups - ```csharp [Test] public async Task ComplexPatternAssertions() @@ -147,7 +141,6 @@ public async Task ComplexPatternAssertions() ## Product Information Validation - ```csharp [Test] public async Task ProductCodeValidation() @@ -159,13 +152,12 @@ public async Task ProductCodeValidation() .Matches(pattern) .And.Group("code", code => code.StartsWith("ABC")) .And.Group("price", price => price.Contains(".99")) - .And.Group("stock", stock => stock.Length().IsEqualTo(2)); + .And.Group("stock", stock => stock.Satisfies(value => Regex.IsMatch(value!, @"^\d{2}$"))); } ``` ## URL Parsing - ```csharp [Test] public async Task UrlParsingAssertions() @@ -187,7 +179,6 @@ public async Task UrlParsingAssertions() The `Matches(string)` overload does not take `RegexOptions`. To apply options like case-insensitivity, construct a `Regex` (or use a source-generated regex) with the desired options and pass it to `Matches`: - ```csharp [Test] public async Task RegexOptionsAssertions() @@ -233,7 +224,6 @@ public partial class MyTests Handle optional capture groups that may be empty: - ```csharp [Test] public async Task OptionalGroupAssertions() @@ -258,7 +248,6 @@ public async Task OptionalGroupAssertions() ## Complete Example - ```csharp [Test] public async Task CompleteEmailValidation() @@ -271,7 +260,7 @@ public async Task CompleteEmailValidation() .And.Group("local", local => local.StartsWith("john")) .And.Group("subdomain", sub => sub.IsEqualTo("mail")) .And.Group("domain", domain => domain.IsEqualTo("example")) - .And.Group("tld", tld => tld.Length().IsEqualTo(3)); + .And.Group("tld", tld => tld.Satisfies(value => Regex.IsMatch(value!, @"^\w{3}$"))); // For position/length checks, use Regex.Match directly var match = System.Text.RegularExpressions.Regex.Match(email, pattern); @@ -284,7 +273,6 @@ public async Task CompleteEmailValidation() The regex assertions surface standard exceptions for common error cases. Wrap the call in an `Assert.That(() => ...)` delegate and assert on the thrown exception type: - ```csharp [Test] public async Task RegexAssertionErrors() diff --git a/docs/docs/assertions/should-syntax.md b/docs/docs/assertions/should-syntax.md index 974ced258f0..c4e01ff1865 100644 --- a/docs/docs/assertions/should-syntax.md +++ b/docs/docs/assertions/should-syntax.md @@ -4,7 +4,6 @@ title: Should Syntax (Optional) description: FluentAssertions-style value.Should().BeEqualTo() syntax via the optional TUnit.Assertions.Should NuGet package. --- - # Should Syntax @@ -56,24 +55,22 @@ The `Does*` strip rule reads naturally for verbs (`DoesMatch` → `Match`, `Does For irregulars or when the conjugation produces an unwanted name, decorate the assertion class with `[ShouldName("...")]`. The override is consulted before the conjugation rules: - ```csharp [AssertionExtension("IsOdd")] [ShouldName("BeAnOddNumber")] -public class OddAssertion : Assertion { … } +public abstract class OddAssertion(AssertionContext context) : Assertion(context) { } ``` `[AssertionExtension(NegatedMethodName = "...")]` produces a second extension method for the negated form, which the Should generator picks up and conjugates independently — `Contains` → `Contain` and `DoesNotContain` → `NotContain` come out automatically without any `[ShouldName]`. When TUnit's pattern uses **separate classes** for positive and negated forms (e.g. `EqualsAssertion` + `NotEqualsAssertion`), place a separate `[ShouldName]` on each: - ```csharp [AssertionExtension("IsBetween")] [ShouldName("BeWithinRange")] -public class BetweenAssertion : Assertion { … } +public abstract class BetweenAssertion(AssertionContext context) : Assertion(context) { } [AssertionExtension("IsNotBetween")] [ShouldName("NotBeWithinRange")] -public class NotBetweenAssertion : Assertion { … } +public abstract class NotBetweenAssertion(AssertionContext context) : Assertion(context) { } ``` ## Entry Points @@ -84,7 +81,7 @@ Each entry overload returns a wrapper appropriate to the source type: // Value entry — returns ShouldSource await 42.Should().BeEqualTo(42); await "hello".Should().Contain("ell"); -await someObject.Should().BeOfType(); +await someObject.Should().BeOfType(typeof(MyClass)); // Collection entry — returns ShouldCollectionSource // exposes element-typed instance methods (BeInOrder, All, Any, @@ -113,7 +110,7 @@ await value .And.NotBeEqualTo(7) .And.BeBetween(1, 10); -await statusCode +await ((int) statusCode) .Should().BeEqualTo(200) .Or.BeEqualTo(201) .Or.BeEqualTo(204); diff --git a/docs/docs/assertions/specialized-types.md b/docs/docs/assertions/specialized-types.md index 097eb7353b4..3593b97f2a8 100644 --- a/docs/docs/assertions/specialized-types.md +++ b/docs/docs/assertions/specialized-types.md @@ -2,7 +2,6 @@ sidebar_position: 12 --- - # Specialized Type Assertions @@ -14,7 +13,6 @@ TUnit provides assertions for many specialized .NET types beyond the common prim Tests whether a GUID is empty (`Guid.Empty`): - ```csharp [Test] public async Task GUID_Is_Empty() @@ -29,7 +27,6 @@ public async Task GUID_Is_Empty() Practical usage: - ```csharp [Test] public async Task Entity_Has_Valid_ID() @@ -47,7 +44,6 @@ public async Task Entity_Has_Valid_ID() Tests for 2xx success status codes: - ```csharp [Test] public async Task HTTP_Success_Status() @@ -60,7 +56,6 @@ public async Task HTTP_Success_Status() Works with all 2xx codes: - ```csharp [Test] public async Task Various_Success_Codes() @@ -74,7 +69,6 @@ public async Task Various_Success_Codes() ### IsNotSuccess - ```csharp [Test] public async Task HTTP_Not_Success() @@ -88,7 +82,6 @@ public async Task HTTP_Not_Success() Tests for 4xx client error status codes: - ```csharp [Test] public async Task HTTP_Client_Error() @@ -104,7 +97,6 @@ public async Task HTTP_Client_Error() Tests for 5xx server error status codes: - ```csharp [Test] public async Task HTTP_Server_Error() @@ -119,7 +111,6 @@ public async Task HTTP_Server_Error() Tests for 3xx redirection status codes: - ```csharp [Test] public async Task HTTP_Redirection() @@ -134,7 +125,6 @@ public async Task HTTP_Redirection() ### IsCancellationRequested / IsNotCancellationRequested - ```csharp [Test] public async Task CancellationToken_Is_Requested() @@ -156,7 +146,6 @@ public async Task CancellationToken_Not_Requested() ### CanBeCanceled / CannotBeCanceled - ```csharp [Test] public async Task Token_Can_Be_Canceled() @@ -179,7 +168,6 @@ public async Task Default_Token_Cannot_Be_Canceled() ### IsLetter / IsNotLetter - ```csharp [Test] public async Task Char_Is_Letter() @@ -194,7 +182,6 @@ public async Task Char_Is_Letter() ### IsDigit / IsNotDigit - ```csharp [Test] public async Task Char_Is_Digit() @@ -208,7 +195,6 @@ public async Task Char_Is_Digit() ### IsWhiteSpace / IsNotWhiteSpace - ```csharp [Test] public async Task Char_Is_WhiteSpace() @@ -223,7 +209,6 @@ public async Task Char_Is_WhiteSpace() ### IsUpper / IsNotUpper - ```csharp [Test] public async Task Char_Is_Upper() @@ -237,7 +222,6 @@ public async Task Char_Is_Upper() ### IsLower / IsNotLower - ```csharp [Test] public async Task Char_Is_Lower() @@ -251,7 +235,6 @@ public async Task Char_Is_Lower() ### IsPunctuation / IsNotPunctuation - ```csharp [Test] public async Task Char_Is_Punctuation() @@ -270,7 +253,6 @@ public async Task Char_Is_Punctuation() #### Exists / DoesNotExist - ```csharp [Test] public async Task Directory_Exists() @@ -291,7 +273,6 @@ public async Task Directory_Does_Not_Exist() #### HasFiles / IsEmpty - ```csharp [Test] public async Task Directory_Has_Files() @@ -316,14 +297,13 @@ public async Task Directory_Is_Empty() #### HasSubdirectories / HasNoSubdirectories - ```csharp [Test] public async Task Directory_Has_Subdirectories() { var windowsDir = new DirectoryInfo(@"C:\Windows"); - await Assert.That(windowsDir).HasSubdirectories(); + await Assert.That(windowsDir.EnumerateDirectories().Any()).IsTrue(); } ``` @@ -331,7 +311,6 @@ public async Task Directory_Has_Subdirectories() #### Exists / DoesNotExist - ```csharp [Test] public async Task File_Exists() @@ -356,7 +335,6 @@ public async Task File_Does_Not_Exist() #### IsReadOnly / IsNotReadOnly - ```csharp [Test] public async Task File_Is_ReadOnly() @@ -377,7 +355,6 @@ public async Task File_Is_ReadOnly() #### IsHidden / IsNotHidden - ```csharp [Test] public async Task File_Is_Hidden() @@ -396,7 +373,6 @@ public async Task File_Is_Hidden() #### IsSystem / IsNotSystem - ```csharp [Test] public async Task File_Is_System() @@ -406,14 +382,13 @@ public async Task File_Is_System() if (systemFile.Exists) { - await Assert.That(systemFile).IsSystem(); + await Assert.That(systemFile.Attributes.HasFlag(FileAttributes.System)).IsTrue(); } } ``` #### IsExecutable / IsNotExecutable - ```csharp [Test] public async Task File_Is_Executable() @@ -422,7 +397,7 @@ public async Task File_Is_Executable() if (exeFile.Exists) { - await Assert.That(exeFile).IsExecutable(); + await Assert.That(exeFile.Extension).IsEqualTo(".exe"); } } ``` @@ -431,14 +406,13 @@ public async Task File_Is_Executable() ### IsIPv4 / IsNotIPv4 - ```csharp [Test] public async Task IP_Is_IPv4() { var ipv4 = IPAddress.Parse("192.168.1.1"); - await Assert.That(ipv4).IsIPv4(); + await Assert.That(ipv4.AddressFamily).IsEqualTo(AddressFamily.InterNetwork); } [Test] @@ -446,20 +420,19 @@ public async Task IP_Not_IPv4() { var ipv6 = IPAddress.Parse("::1"); - await Assert.That(ipv6).IsNotIPv4(); + await Assert.That(ipv6.AddressFamily).IsNotEqualTo(AddressFamily.InterNetwork); } ``` ### IsIPv6 / IsNotIPv6 - ```csharp [Test] public async Task IP_Is_IPv6() { var ipv6 = IPAddress.Parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); - await Assert.That(ipv6).IsIPv6(); + await Assert.That(ipv6.AddressFamily).IsEqualTo(AddressFamily.InterNetworkV6); } [Test] @@ -467,7 +440,7 @@ public async Task IP_Not_IPv6() { var ipv4 = IPAddress.Parse("127.0.0.1"); - await Assert.That(ipv4).IsNotIPv6(); + await Assert.That(ipv4.AddressFamily).IsNotEqualTo(AddressFamily.InterNetworkV6); } ``` @@ -475,14 +448,13 @@ public async Task IP_Not_IPv6() ### IsValueCreated / IsNotValueCreated - ```csharp [Test] public async Task Lazy_Value_Not_Created() { var lazy = new Lazy(() => 42); - await Assert.That(lazy).IsNotValueCreated(); + await Assert.That(lazy.IsValueCreated).IsFalse(); var value = lazy.Value; @@ -495,27 +467,25 @@ public async Task Lazy_Value_Not_Created() ### CanRead / CannotRead - ```csharp [Test] public async Task Stream_Can_Read() { using var stream = new MemoryStream(); - await Assert.That(stream).CanRead(); + await Assert.That((Stream) stream).CanRead(); } ``` ### CanWrite / CannotWrite - ```csharp [Test] public async Task Stream_Can_Write() { using var stream = new MemoryStream(); - await Assert.That(stream).CanWrite(); + await Assert.That((Stream) stream).CanWrite(); } [Test] @@ -523,26 +493,24 @@ public async Task Stream_Cannot_Write() { var readOnlyStream = new MemoryStream(new byte[10], writable: false); - await Assert.That(readOnlyStream).CannotWrite(); + await Assert.That((Stream) readOnlyStream).CannotWrite(); } ``` ### CanSeek / CannotSeek - ```csharp [Test] public async Task Stream_Can_Seek() { using var stream = new MemoryStream(); - await Assert.That(stream).CanSeek(); + await Assert.That((Stream) stream).CanSeek(); } ``` ### CanTimeout / CannotTimeout - ```csharp [Test] public async Task Network_Stream_Can_Timeout() @@ -557,7 +525,6 @@ public async Task Network_Stream_Can_Timeout() ### HasExited / HasNotExited - ```csharp [Test] public async Task Process_Has_Not_Exited() @@ -575,14 +542,13 @@ public async Task Process_Has_Not_Exited() ### IsResponding / IsNotResponding - ```csharp [Test] public async Task Process_Is_Responding() { var process = Process.GetCurrentProcess(); - await Assert.That(process).IsResponding(); + await Assert.That(process.Responding).IsTrue(); } ``` @@ -590,7 +556,6 @@ public async Task Process_Is_Responding() ### IsAlive / IsNotAlive - ```csharp [Test] public async Task Thread_Is_Alive() @@ -607,7 +572,6 @@ public async Task Thread_Is_Alive() ### IsBackground / IsNotBackground - ```csharp [Test] public async Task Thread_Is_Background() @@ -621,7 +585,6 @@ public async Task Thread_Is_Background() ### IsThreadPoolThread / IsNotThreadPoolThread - ```csharp [Test] public async Task Check_ThreadPool_Thread() @@ -637,7 +600,6 @@ public async Task Check_ThreadPool_Thread() ### IsAlive / IsNotAlive - ```csharp [Test] public async Task WeakReference_Is_Alive() @@ -659,7 +621,6 @@ public async Task WeakReference_Is_Alive() ### IsAbsoluteUri / IsNotAbsoluteUri - ```csharp [Test] public async Task URI_Is_Absolute() @@ -682,14 +643,13 @@ public async Task URI_Is_Relative() ### IsUtf8 / IsNotUtf8 - ```csharp [Test] public async Task Encoding_Is_UTF8() { var encoding = Encoding.UTF8; - await Assert.That(encoding).IsUtf8(); + await Assert.That(encoding.WebName).IsEqualTo(Encoding.UTF8.WebName); } [Test] @@ -697,7 +657,7 @@ public async Task Encoding_Not_UTF8() { var encoding = Encoding.ASCII; - await Assert.That(encoding).IsNotUtf8(); + await Assert.That(encoding.WebName).IsNotEqualTo(Encoding.UTF8.WebName); } ``` @@ -705,7 +665,6 @@ public async Task Encoding_Not_UTF8() Version comparisons using standard comparison operators: - ```csharp [Test] public async Task Version_Comparison() @@ -722,7 +681,6 @@ public async Task Version_Comparison() ### IsWeekend / IsNotWeekend - ```csharp [Test] public async Task Day_Is_Weekend() @@ -734,7 +692,6 @@ public async Task Day_Is_Weekend() ### IsWeekday / IsNotWeekday - ```csharp [Test] public async Task Day_Is_Weekday() @@ -751,7 +708,6 @@ public async Task Day_Is_Weekday() ### API Testing - ```csharp [Test] public async Task API_Returns_Success() @@ -765,7 +721,6 @@ public async Task API_Returns_Success() ### File Upload Validation - ```csharp [Test] public async Task Uploaded_File_Validation() @@ -780,7 +735,6 @@ public async Task Uploaded_File_Validation() ### Configuration Directory Check - ```csharp [Test] public async Task Config_Directory_Setup() @@ -794,14 +748,13 @@ public async Task Config_Directory_Setup() ### Network Validation - ```csharp [Test] public async Task Server_IP_Is_Valid() { - var serverIp = IPAddress.Parse(Configuration["ServerIP"]); + var serverIp = IPAddress.Parse(Configuration["ServerIP"] ?? "127.0.0.1"); - await Assert.That(serverIp).IsIPv4(); + await Assert.That(serverIp.AddressFamily).IsEqualTo(AddressFamily.InterNetwork); } ``` diff --git a/docs/docs/assertions/string.md b/docs/docs/assertions/string.md index e67c4263056..5f5412888fc 100644 --- a/docs/docs/assertions/string.md +++ b/docs/docs/assertions/string.md @@ -2,7 +2,6 @@ sidebar_position: 5.5 --- - # String Assertions @@ -14,7 +13,6 @@ TUnit provides rich assertions for testing strings, including substring matching Tests that a string contains a substring: - ```csharp [Test] public async Task String_Contains() @@ -29,7 +27,6 @@ public async Task String_Contains() #### Case-Insensitive Contains - ```csharp [Test] public async Task Contains_Ignoring_Case() @@ -48,7 +45,6 @@ public async Task Contains_Ignoring_Case() #### With String Comparison - ```csharp [Test] public async Task Contains_With_Comparison() @@ -63,7 +59,6 @@ public async Task Contains_With_Comparison() #### With Trimming - ```csharp [Test] public async Task Contains_With_Trimming() @@ -78,7 +73,6 @@ public async Task Contains_With_Trimming() #### Ignoring Whitespace - ```csharp [Test] public async Task Contains_Ignoring_Whitespace() @@ -95,7 +89,6 @@ public async Task Contains_Ignoring_Whitespace() Tests that a string does not contain a substring: - ```csharp [Test] public async Task String_Does_Not_Contain() @@ -109,7 +102,6 @@ public async Task String_Does_Not_Contain() All modifiers work with `DoesNotContain`: - ```csharp [Test] public async Task Does_Not_Contain_Ignoring_Case() @@ -126,7 +118,6 @@ public async Task Does_Not_Contain_Ignoring_Case() Tests that a string starts with a specific prefix: - ```csharp [Test] public async Task String_Starts_With() @@ -142,7 +133,6 @@ public async Task String_Starts_With() With case-insensitive comparison: - ```csharp [Test] public async Task Starts_With_Ignoring_Case() @@ -159,7 +149,6 @@ public async Task Starts_With_Ignoring_Case() Tests that a string ends with a specific suffix: - ```csharp [Test] public async Task String_Ends_With() @@ -175,7 +164,6 @@ public async Task String_Ends_With() With case-insensitive comparison: - ```csharp [Test] public async Task Ends_With_Ignoring_Case() @@ -194,7 +182,6 @@ public async Task Ends_With_Ignoring_Case() Tests that a string matches a regular expression pattern: - ```csharp [Test] public async Task String_Matches_Pattern() @@ -207,7 +194,6 @@ public async Task String_Matches_Pattern() With a compiled `Regex`: - ```csharp [Test] public async Task Matches_With_Regex() @@ -221,7 +207,6 @@ public async Task Matches_With_Regex() #### Case-Insensitive Matching - ```csharp [Test] public async Task Matches_Ignoring_Case() @@ -236,7 +221,6 @@ public async Task Matches_Ignoring_Case() #### With Regex Options - ```csharp [Test] public async Task Matches_With_Options() @@ -253,7 +237,6 @@ public async Task Matches_With_Options() Tests that a string does not match a pattern: - ```csharp [Test] public async Task String_Does_Not_Match() @@ -270,7 +253,6 @@ public async Task String_Does_Not_Match() Tests that a string is empty (`""`): - ```csharp [Test] public async Task String_Is_Empty() @@ -283,7 +265,6 @@ public async Task String_Is_Empty() Note: This checks for an empty string, not `null`: - ```csharp [Test] public async Task Empty_vs_Null() @@ -301,7 +282,6 @@ public async Task Empty_vs_Null() Tests that a string is not empty: - ```csharp [Test] public async Task String_Is_Not_Empty() @@ -316,7 +296,6 @@ public async Task String_Is_Not_Empty() Tests that a string has a specific length: - ```csharp [Test] public async Task String_Has_Length() @@ -329,22 +308,19 @@ public async Task String_Has_Length() With chained comparison: - ```csharp [Test] public async Task Length_With_Comparison() { var username = "alice"; - await Assert.That(username) - .Length().IsGreaterThan(3) - .And.Length().IsLessThan(20); + await Assert.That(username).Length().IsGreaterThan(3); + await Assert.That(username).Length().IsLessThan(20); } ``` Using `IsBetween`: - ```csharp [Test] public async Task Length_Range() @@ -361,7 +337,6 @@ public async Task Length_Range() String equality with various comparison options: - ```csharp [Test] public async Task String_Equality() @@ -375,7 +350,6 @@ public async Task String_Equality() #### Ignoring Case - ```csharp [Test] public async Task Equality_Ignoring_Case() @@ -391,7 +365,6 @@ public async Task Equality_Ignoring_Case() #### With String Comparison - ```csharp [Test] public async Task Equality_With_Comparison() @@ -409,7 +382,6 @@ public async Task Equality_With_Comparison() You can parse strings to other types and assert on the result: - ```csharp [Test] public async Task Parse_String_To_Int() @@ -421,7 +393,6 @@ public async Task Parse_String_To_Int() } ``` - ```csharp [Test] public async Task Parse_String_To_DateTime() @@ -437,23 +408,19 @@ public async Task Parse_String_To_DateTime() ### Email Validation - ```csharp [Test] public async Task Validate_Email() { var email = "user@example.com"; - await Assert.That(email) - .Contains("@") - .And.Matches(@"^[\w\.-]+@[\w\.-]+\.\w+$") - .And.DoesNotContain(" "); + await Assert.That(email).Contains("@").And.DoesNotContain(" "); + await Assert.That(email).Matches(@"^[\w\.-]+@[\w\.-]+\.\w+$"); } ``` ### URL Validation - ```csharp [Test] public async Task Validate_URL() @@ -469,7 +436,6 @@ public async Task Validate_URL() ### File Extension Check - ```csharp [Test] public async Task Check_File_Extension() @@ -484,42 +450,37 @@ public async Task Check_File_Extension() ### Username Validation - ```csharp [Test] public async Task Validate_Username() { var username = "alice_123"; - await Assert.That(username) - .Length().IsGreaterThanOrEqualTo(3) - .And.Length().IsLessThanOrEqualTo(20) - .And.Matches(@"^[a-zA-Z0-9_]+$") - .And.DoesNotContain(" "); + await Assert.That(username).Length().IsGreaterThanOrEqualTo(3); + await Assert.That(username).Length().IsLessThanOrEqualTo(20); + await Assert.That(username).Matches(@"^[a-zA-Z0-9_]+$"); + await Assert.That(username).DoesNotContain(" "); } ``` ### Password Requirements - ```csharp [Test] public async Task Validate_Password() { var password = "SecureP@ss123"; - await Assert.That(password) - .Length().IsGreaterThanOrEqualTo(8) - .And.Matches(@"[A-Z]") // Has uppercase - .And.Matches(@"[a-z]") // Has lowercase - .And.Matches(@"\d") // Has digit - .And.Matches(@"[@$!%*?&]"); // Has special char + await Assert.That(password).Length().IsGreaterThanOrEqualTo(8); + await Assert.That(password).Matches(@"[A-Z]"); // Has uppercase + await Assert.That(password).Matches(@"[a-z]"); // Has lowercase + await Assert.That(password).Matches(@"\d"); // Has digit + await Assert.That(password).Matches(@"[@$!%*?&]"); // Has special char } ``` ### JSON String Content - ```csharp [Test] public async Task Check_JSON_Content() @@ -536,7 +497,6 @@ public async Task Check_JSON_Content() ### SQL Query Validation - ```csharp [Test] public async Task Validate_SQL_Query() @@ -554,7 +514,6 @@ public async Task Validate_SQL_Query() ### IsNullOrEmpty Equivalent - ```csharp [Test] public async Task Check_Null_Or_Empty() @@ -576,7 +535,6 @@ public async Task Check_Null_Or_Empty() ### IsNullOrWhiteSpace Equivalent - ```csharp [Test] public async Task Check_Null_Or_Whitespace() @@ -589,7 +547,6 @@ public async Task Check_Null_Or_Whitespace() Better with trimming: - ```csharp [Test] public async Task Require_Non_Whitespace() @@ -609,7 +566,6 @@ public async Task Require_Non_Whitespace() TUnit also supports assertions on `StringBuilder`: - ```csharp [Test] public async Task StringBuilder_Tests() @@ -628,7 +584,6 @@ public async Task StringBuilder_Tests() ## Chaining String Assertions - ```csharp [Test] public async Task Chained_String_Assertions() @@ -647,7 +602,6 @@ public async Task Chained_String_Assertions() ## Case Sensitivity Patterns - ```csharp [Test] public async Task Case_Sensitivity() @@ -671,7 +625,6 @@ public async Task Case_Sensitivity() ## String Formatting Validation - ```csharp [Test] public async Task Formatted_String() @@ -689,7 +642,6 @@ public async Task Formatted_String() ## Multi-line Strings - ```csharp [Test] public async Task Multiline_String() @@ -709,7 +661,6 @@ public async Task Multiline_String() ## Common String Comparison Options - ```csharp [Test] public async Task String_Comparison_Options() @@ -735,7 +686,6 @@ public async Task String_Comparison_Options() ## Path Validation - ```csharp [Test] public async Task File_Path_Validation() @@ -751,7 +701,6 @@ public async Task File_Path_Validation() Unix path: - ```csharp [Test] public async Task Unix_Path_Validation() @@ -769,7 +718,6 @@ public async Task Unix_Path_Validation() ### Trim and Assert - ```csharp [Test] public async Task Trim_Before_Assert() @@ -783,7 +731,6 @@ public async Task Trim_Before_Assert() ### Case Normalization - ```csharp [Test] public async Task Normalize_Case() @@ -797,7 +744,6 @@ public async Task Normalize_Case() ### Substring Extraction - ```csharp [Test] public async Task Extract_Substring() diff --git a/docs/docs/assertions/tasks-and-async.md b/docs/docs/assertions/tasks-and-async.md index 04d34be5cec..b5ac9b79fa8 100644 --- a/docs/docs/assertions/tasks-and-async.md +++ b/docs/docs/assertions/tasks-and-async.md @@ -2,7 +2,6 @@ sidebar_position: 11 --- - # Task and Async Assertions @@ -14,7 +13,6 @@ TUnit provides specialized assertions for testing `Task` and `Task` objects, Tests whether a task has completed (successfully, faulted, or canceled): - ```csharp [Test] public async Task Task_Is_Completed() @@ -31,7 +29,6 @@ public async Task Task_Is_Completed() Tests whether a task was canceled: - ```csharp [Test] public async Task Task_Is_Canceled() @@ -54,7 +51,6 @@ public async Task Task_Is_Canceled() } ``` - ```csharp [Test] public async Task Task_Not_Canceled() @@ -69,7 +65,6 @@ public async Task Task_Not_Canceled() Tests whether a task ended in a faulted state (threw an exception): - ```csharp [Test] public async Task Task_Is_Faulted() @@ -89,7 +84,6 @@ public async Task Task_Is_Faulted() } ``` - ```csharp [Test] public async Task Task_Not_Faulted() @@ -104,7 +98,6 @@ public async Task Task_Not_Faulted() Tests whether a task completed successfully (not faulted or canceled): - ```csharp [Test] public async Task Task_Completed_Successfully() @@ -115,7 +108,6 @@ public async Task Task_Completed_Successfully() } ``` - ```csharp [Test] public async Task Task_Not_Completed_Successfully() @@ -134,7 +126,6 @@ public async Task Task_Not_Completed_Successfully() Tests that a task completes within a specified time: - ```csharp [Test] public async Task Task_Completes_Within_Timeout() @@ -147,7 +138,6 @@ public async Task Task_Completes_Within_Timeout() Fails if timeout exceeded: - ```csharp [Test] public async Task Task_Exceeds_Timeout() @@ -163,7 +153,6 @@ public async Task Task_Exceeds_Timeout() Polls a value source until a nested assertion passes or the timeout expires. `WaitsFor` takes an assertion-builder lambda (not a bool predicate), so you write the same fluent assertions you would elsewhere: - ```csharp [Test] public async Task Wait_For_Condition() @@ -187,14 +176,13 @@ public async Task Wait_For_Condition() ### API Call Timeout - ```csharp [Test] public async Task API_Call_Completes_In_Time() { var apiTask = _httpClient.GetAsync("https://api.example.com/data"); - await Assert.That(apiTask).CompletesWithin(TimeSpan.FromSeconds(5)); + await Assert.That((Func)(async () => { await apiTask; })).CompletesWithin(TimeSpan.FromSeconds(5)); var response = await apiTask; await Assert.That(response.IsSuccessStatusCode).IsTrue(); @@ -203,7 +191,6 @@ public async Task API_Call_Completes_In_Time() ### Background Task Completion - ```csharp [Test] public async Task Background_Processing_Completes() @@ -217,7 +204,6 @@ public async Task Background_Processing_Completes() ### Cancellation Token Handling - ```csharp [Test] public async Task Operation_Respects_Cancellation() @@ -244,7 +230,6 @@ public async Task Operation_Respects_Cancellation() For testing exceptions in async code, use exception assertions: - ```csharp [Test] public async Task Async_Method_Throws_Exception() @@ -258,7 +243,6 @@ public async Task Async_Method_Throws_Exception() For `Task`, await the task first, then assert on the result: - ```csharp [Test] public async Task Task_Returns_Expected_Result() @@ -266,7 +250,7 @@ public async Task Task_Returns_Expected_Result() var task = GetValueAsync(); // Ensure it completes in time - await Assert.That(task).CompletesWithin(TimeSpan.FromSeconds(1)); + await Assert.That((Func)(async () => { await task; })).CompletesWithin(TimeSpan.FromSeconds(1)); // Get the result var result = await task; @@ -278,7 +262,6 @@ public async Task Task_Returns_Expected_Result() ### Parallel Task Execution - ```csharp [Test] public async Task Parallel_Tasks_Complete() @@ -296,7 +279,6 @@ public async Task Parallel_Tasks_Complete() ### Task State Transitions - ```csharp [Test] public async Task Task_State_Progression() @@ -321,7 +303,6 @@ public async Task Task_State_Progression() ### Failed Task - ```csharp [Test] public async Task Task_Fails_With_Exception() @@ -338,7 +319,6 @@ public async Task Task_Fails_With_Exception() ### Canceled Task - ```csharp [Test] public async Task Task_Can_Be_Canceled() @@ -357,7 +337,6 @@ public async Task Task_Can_Be_Canceled() ### WhenAll Completion - ```csharp [Test] public async Task All_Tasks_Complete() @@ -374,7 +353,6 @@ public async Task All_Tasks_Complete() ### WhenAny Completion - ```csharp [Test] public async Task Any_Task_Completes() @@ -384,7 +362,7 @@ public async Task Any_Task_Completes() var firstCompleted = Task.WhenAny(fastTask, slowTask); - await Assert.That(firstCompleted).CompletesWithin(TimeSpan.FromMilliseconds(500)); + await Assert.That((Func)(async () => { await firstCompleted; })).CompletesWithin(TimeSpan.FromMilliseconds(500)); var completed = await firstCompleted; await Assert.That(completed).IsSameReferenceAs(fastTask); @@ -395,7 +373,6 @@ public async Task Any_Task_Completes() `ValueTask` and `ValueTask` work similarly: - ```csharp [Test] public async Task ValueTask_Completion() @@ -415,14 +392,13 @@ async ValueTask GetValueTaskAsync() ## Chaining Task Assertions - ```csharp [Test] public async Task Chained_Task_Assertions() { var task = GetDataAsync(); - await Assert.That(task) + await Assert.That((Func)(async () => { await task; })) .CompletesWithin(TimeSpan.FromSeconds(5)); await Assert.That(task) @@ -437,7 +413,6 @@ public async Task Chained_Task_Assertions() ### Retry Logic Testing - ```csharp [Test] public async Task Retry_Eventually_Succeeds() @@ -452,7 +427,7 @@ public async Task Retry_Eventually_Succeeds() return "Success"; }, maxRetries: 5); - await Assert.That(task).CompletesWithin(TimeSpan.FromSeconds(10)); + await Assert.That((Func)(async () => { await task; })).CompletesWithin(TimeSpan.FromSeconds(10)); var result = await task; await Assert.That(result).IsEqualTo("Success"); } @@ -460,7 +435,6 @@ public async Task Retry_Eventually_Succeeds() ### Debounce Testing - ```csharp [Test] public async Task Debounced_Operation() @@ -473,14 +447,13 @@ public async Task Debounced_Operation() trigger.OnNext("value"); - await Assert.That(debouncedTask) + await Assert.That((Func)(async () => { await debouncedTask; })) .CompletesWithin(TimeSpan.FromSeconds(1)); } ``` ### Circuit Breaker Testing - ```csharp [Test] public async Task Circuit_Breaker_Opens() @@ -507,7 +480,6 @@ public async Task Circuit_Breaker_Opens() ### Producer-Consumer Testing - ```csharp [Test] public async Task Producer_Consumer_Processes_Items() @@ -524,7 +496,6 @@ public async Task Producer_Consumer_Processes_Items() ### Rate Limiting - ```csharp [Test] public async Task Rate_Limiter_Delays_Requests() @@ -546,7 +517,6 @@ public async Task Rate_Limiter_Delays_Requests() ## Testing Async Disposal - ```csharp [Test] public async Task Async_Disposable_Cleanup() diff --git a/docs/docs/assertions/type-checking.md b/docs/docs/assertions/type-checking.md index d908b104b0f..1a81b920e50 100644 --- a/docs/docs/assertions/type-checking.md +++ b/docs/docs/assertions/type-checking.md @@ -2,7 +2,6 @@ sidebar_position: 5 --- - # Type Checking @@ -10,7 +9,6 @@ TUnit assertions check types at compile time wherever possible. This gives faste For example, this wouldn't compile because we're comparing an `int` and a `string`: - ```csharp [Test] public async Task MyTest() @@ -27,7 +25,6 @@ When you need to verify types at runtime — for example, when working with poly Tests that a value is exactly the specified type (not a subclass): - ```csharp [Test] public async Task Exact_Type() @@ -42,7 +39,6 @@ public async Task Exact_Type() 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() @@ -58,7 +54,6 @@ public async Task Assignable_To_Base_Or_Interface() Tests that a value is **not** exactly the specified type: - ```csharp [Test] public async Task Not_Exact_Type() @@ -73,7 +68,6 @@ public async Task Not_Exact_Type() Tests that a value cannot be assigned to the specified type: - ```csharp [Test] public async Task Not_Assignable() @@ -88,7 +82,6 @@ public async Task Not_Assignable() Tests that a value of the specified type can be assigned to a variable of this value's type. This is the reverse of `IsAssignableTo`: - ```csharp [Test] public async Task Assignable_From_Derived() @@ -104,7 +97,6 @@ public async Task Assignable_From_Derived() Tests that a value of the specified type cannot be assigned to a variable of this value's type: - ```csharp [Test] public async Task Not_Assignable_From() @@ -119,7 +111,6 @@ public async Task Not_Assignable_From() 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() diff --git a/docs/docs/assertions/types.md b/docs/docs/assertions/types.md index c6f190157b8..7a0efed6ea5 100644 --- a/docs/docs/assertions/types.md +++ b/docs/docs/assertions/types.md @@ -2,7 +2,6 @@ sidebar_position: 9 --- - # Type Assertions @@ -14,7 +13,6 @@ TUnit provides comprehensive assertions for testing types and type properties. T Tests that a value is exactly of a specific type: - ```csharp [Test] public async Task Value_Is_Type() @@ -27,7 +25,6 @@ public async Task Value_Is_Type() Works with all types: - ```csharp [Test] public async Task Various_Types() @@ -43,7 +40,6 @@ public async Task Various_Types() Tests that a type can be assigned to a target type (inheritance/interface): - ```csharp [Test] public async Task Type_Is_Assignable() @@ -58,7 +54,6 @@ public async Task Type_Is_Assignable() With inheritance: - ```csharp public class Animal { } public class Dog : Animal { } @@ -78,7 +73,6 @@ public async Task Inheritance_Assignability() Tests that a type cannot be assigned to a target type: - ```csharp [Test] public async Task Type_Not_Assignable() @@ -94,7 +88,6 @@ public async Task Type_Not_Assignable() All the following assertions work on `Type` objects directly: - ```csharp [Test] public async Task Type_Object_Assertions() @@ -110,7 +103,6 @@ public async Task Type_Object_Assertions() #### IsClass / IsNotClass - ```csharp [Test] public async Task Is_Class() @@ -126,7 +118,6 @@ public async Task Is_Class() #### IsInterface / IsNotInterface - ```csharp [Test] public async Task Is_Interface() @@ -142,7 +133,6 @@ public async Task Is_Interface() #### IsAbstract / IsNotAbstract - ```csharp public abstract class AbstractBase { } public class Concrete : AbstractBase { } @@ -157,7 +147,6 @@ public async Task Is_Abstract() #### IsSealed / IsNotSealed - ```csharp public sealed class SealedClass { } public class OpenClass { } @@ -175,7 +164,6 @@ public async Task Is_Sealed() #### IsValueType / IsNotValueType - ```csharp [Test] public async Task Is_Value_Type() @@ -191,7 +179,6 @@ public async Task Is_Value_Type() #### IsEnum / IsNotEnum - ```csharp public enum Color { Red, Green, Blue } @@ -207,7 +194,6 @@ public async Task Is_Enum() #### IsPrimitive / IsNotPrimitive - ```csharp [Test] public async Task Is_Primitive() @@ -227,7 +213,6 @@ public async Task Is_Primitive() #### IsPublic / IsNotPublic - ```csharp public class PublicClass { } internal class InternalClass { } @@ -246,7 +231,6 @@ public async Task Is_Public() #### IsGenericType / IsNotGenericType - ```csharp [Test] public async Task Is_Generic_Type() @@ -260,7 +244,6 @@ public async Task Is_Generic_Type() #### IsGenericTypeDefinition / IsNotGenericTypeDefinition - ```csharp [Test] public async Task Is_Generic_Type_Definition() @@ -276,7 +259,6 @@ public async Task Is_Generic_Type_Definition() #### IsConstructedGenericType / IsNotConstructedGenericType - ```csharp [Test] public async Task Is_Constructed_Generic_Type() @@ -291,7 +273,6 @@ public async Task Is_Constructed_Generic_Type() #### ContainsGenericParameters / DoesNotContainGenericParameters - ```csharp [Test] public async Task Contains_Generic_Parameters() @@ -307,7 +288,6 @@ public async Task Contains_Generic_Parameters() #### IsArray / IsNotArray - ```csharp [Test] public async Task Is_Array() @@ -322,7 +302,6 @@ public async Task Is_Array() #### IsByRef / IsNotByRef - ```csharp [Test] public async Task Is_By_Ref() @@ -337,7 +316,6 @@ public async Task Is_By_Ref() #### IsByRefLike / IsNotByRefLike (.NET 5+) - ```csharp [Test] public async Task Is_By_Ref_Like() @@ -351,17 +329,17 @@ public async Task Is_By_Ref_Like() #### IsPointer / IsNotPointer - ```csharp [Test] public async Task Is_Pointer() { + Type intPtr; unsafe { - var intPtr = typeof(int*); - await Assert.That(intPtr).IsPointer(); + intPtr = typeof(int*); } + await Assert.That(intPtr).IsPointer(); await Assert.That(typeof(int)).IsNotPointer(); } ``` @@ -370,7 +348,6 @@ public async Task Is_Pointer() #### IsNested / IsNotNested - ```csharp public class Outer { @@ -387,7 +364,6 @@ public async Task Is_Nested() #### IsNestedPublic / IsNotNestedPublic - ```csharp public class Container { @@ -404,7 +380,6 @@ public async Task Is_Nested_Public() #### IsNestedPrivate / IsNotNestedPrivate - ```csharp [Test] public async Task Is_Nested_Private() @@ -428,7 +403,6 @@ For protected nested types. #### IsVisible / IsNotVisible - ```csharp [Test] public async Task Is_Visible() @@ -437,7 +411,7 @@ public async Task Is_Visible() await Assert.That(typeof(List)).IsVisible(); // Internal types are not visible - var internalType = Assembly.GetExecutingAssembly() + var internalType = System.Reflection.Assembly.GetExecutingAssembly() .GetTypes() .FirstOrDefault(t => !t.IsPublic && !t.IsNested); @@ -452,7 +426,6 @@ public async Task Is_Visible() #### IsCOMObject / IsNotCOMObject - ```csharp [Test] public async Task Is_COM_Object() @@ -466,7 +439,6 @@ public async Task Is_COM_Object() ### Dependency Injection Validation - ```csharp [Test] public async Task Service_Implements_Interface() @@ -480,7 +452,6 @@ public async Task Service_Implements_Interface() ### Plugin System - ```csharp public interface IPlugin { } @@ -497,7 +468,6 @@ public async Task Plugin_Implements_Interface() ### Reflection Testing - ```csharp [Test] public async Task Type_Has_Expected_Properties() @@ -513,7 +483,6 @@ public async Task Type_Has_Expected_Properties() ### Generic Constraints - ```csharp [Test] public async Task Validate_Generic_Constraints() @@ -527,7 +496,6 @@ public async Task Validate_Generic_Constraints() ### Enum Validation - ```csharp [Test] public async Task Type_Is_Enum() @@ -541,7 +509,6 @@ public async Task Type_Is_Enum() ### Abstract Class Validation - ```csharp [Test] public async Task Base_Class_Is_Abstract() @@ -555,7 +522,6 @@ public async Task Base_Class_Is_Abstract() ## Chaining Type Assertions - ```csharp [Test] public async Task Chained_Type_Assertions() @@ -572,7 +538,6 @@ public async Task Chained_Type_Assertions() ## Type Comparison - ```csharp [Test] public async Task Compare_Types() @@ -588,7 +553,6 @@ public async Task Compare_Types() ## Working with Base Types - ```csharp [Test] public async Task Check_Base_Type() @@ -602,7 +566,6 @@ public async Task Check_Base_Type() ## Interface Implementation - ```csharp [Test] public async Task Implements_Multiple_Interfaces() @@ -619,12 +582,11 @@ public async Task Implements_Multiple_Interfaces() ### Factory Pattern Validation - ```csharp [Test] public async Task Factory_Returns_Correct_Type() { - var instance = Factory.Create("user-service"); + var instance = new ExampleFactory().Create("user-service"); await Assert.That(instance).IsTypeOf(); await Assert.That(instance).IsAssignableTo(); @@ -633,7 +595,6 @@ public async Task Factory_Returns_Correct_Type() ### ORM Entity Validation - ```csharp [Test] public async Task Entity_Is_Properly_Configured() @@ -651,7 +612,6 @@ public async Task Entity_Is_Properly_Configured() ### Serialization Requirements - ```csharp [Test] public async Task Type_Is_Serializable() @@ -669,7 +629,6 @@ public async Task Type_Is_Serializable() ### Test Double Validation - ```csharp [Test] public async Task Mock_Implements_Interface() @@ -683,7 +642,6 @@ public async Task Mock_Implements_Interface() ## Struct Validation - ```csharp public struct Point { @@ -704,7 +662,6 @@ public async Task Struct_Properties() ## Record Validation - ```csharp public record Person(string Name, int Age); diff --git a/docs/docs/benchmarks/engine/AsyncTests.md b/docs/docs/benchmarks/engine/AsyncTests.md index 7a4bdd21070..e4aaef0bfd5 100644 --- a/docs/docs/benchmarks/engine/AsyncTests.md +++ b/docs/docs/benchmarks/engine/AsyncTests.md @@ -9,7 +9,7 @@ sidebar_position: 3 > Realistic async/await patterns with I/O simulation :::info Last Updated -This benchmark was automatically generated on **2026-08-23** from the latest CI run. +This benchmark was automatically generated on **2026-08-30** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -18,12 +18,12 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.65.38 | 358.9 ms | 358.6 ms | 2.79 ms | -| NUnit | 4.6.1 | 577.1 ms | 574.3 ms | 9.26 ms | -| MSTest | 4.3.3 | 678.0 ms | 673.8 ms | 20.11 ms | -| xUnit3 | 4.0.0 | 737.0 ms | 733.0 ms | 27.93 ms | -| **TUnit (AOT)** | 1.65.38 | 118.5 ms | 118.6 ms | 1.06 ms | -| xUnit3_AOT | 4.0.0 | 120.1 ms | 120.0 ms | 1.23 ms | +| **TUnit** | 1.65.68 | 388.5 ms | 386.4 ms | 22.97 ms | +| NUnit | 4.6.1 | 714.3 ms | 707.6 ms | 19.79 ms | +| MSTest | 4.3.3 | 664.8 ms | 664.0 ms | 5.74 ms | +| xUnit3 | 4.0.0 | 730.9 ms | 733.0 ms | 16.56 ms | +| **TUnit (AOT)** | 1.65.68 | 116.0 ms | 116.0 ms | 0.29 ms | +| xUnit3_AOT | 4.0.0 | 118.5 ms | 118.5 ms | 0.68 ms | ## 📈 Visual Comparison @@ -61,8 +61,8 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI xychart-beta title "AsyncTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT", "xUnit3_AOT"] - y-axis "Time (ms)" 0 --> 885 - bar [358.9, 577.1, 678, 737, 118.5, 120.1] + y-axis "Time (ms)" 0 --> 878 + bar [388.5, 714.3, 664.8, 730.9, 116, 118.5] ``` ## 🎯 Key Insights @@ -75,4 +75,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3, xUnit View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2026-08-23T00:20:42.432Z* +*Last generated: 2026-08-30T00:32:59.981Z* diff --git a/docs/docs/benchmarks/engine/BuildTime.md b/docs/docs/benchmarks/engine/BuildTime.md index 6c19b52ec9e..7e06da9343d 100644 --- a/docs/docs/benchmarks/engine/BuildTime.md +++ b/docs/docs/benchmarks/engine/BuildTime.md @@ -9,7 +9,7 @@ sidebar_position: 9 > Compilation time from a clean build across frameworks — how long it takes to build an identical test project. :::info Last Updated -This benchmark was automatically generated on **2026-08-23** from the latest CI run. +This benchmark was automatically generated on **2026-08-30** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -20,10 +20,10 @@ Compilation time comparison across frameworks: | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.65.38 | 917.6 ms | 918.9 ms | 26.90 ms | -| Build_NUnit | 4.6.1 | 893.9 ms | 899.1 ms | 12.39 ms | -| Build_MSTest | 4.3.3 | 1,036.7 ms | 1,037.3 ms | 13.77 ms | -| Build_xUnit3 | 4.0.0 | 862.9 ms | 861.3 ms | 12.60 ms | +| **TUnit** | 1.65.68 | 923.9 ms | 915.4 ms | 34.98 ms | +| Build_NUnit | 4.6.1 | 884.6 ms | 885.1 ms | 9.95 ms | +| Build_MSTest | 4.3.3 | 1,026.0 ms | 1,019.4 ms | 47.11 ms | +| Build_xUnit3 | 4.0.0 | 879.1 ms | 881.5 ms | 8.46 ms | ## 📈 Visual Comparison @@ -61,8 +61,8 @@ Compilation time comparison across frameworks: xychart-beta title "Build Time Comparison" x-axis ["Build_TUnit", "Build_NUnit", "Build_MSTest", "Build_xUnit3"] - y-axis "Time (ms)" 0 --> 1245 - bar [917.6, 893.9, 1036.7, 862.9] + y-axis "Time (ms)" 0 --> 1232 + bar [923.9, 884.6, 1026, 879.1] ``` --- @@ -71,4 +71,4 @@ xychart-beta View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2026-08-23T00:20:42.433Z* +*Last generated: 2026-08-30T00:32:59.983Z* diff --git a/docs/docs/benchmarks/engine/DataDrivenTests.md b/docs/docs/benchmarks/engine/DataDrivenTests.md index 5a3f44ace64..d779989b434 100644 --- a/docs/docs/benchmarks/engine/DataDrivenTests.md +++ b/docs/docs/benchmarks/engine/DataDrivenTests.md @@ -9,7 +9,7 @@ sidebar_position: 4 > Parameterized tests with multiple data sources :::info Last Updated -This benchmark was automatically generated on **2026-08-23** from the latest CI run. +This benchmark was automatically generated on **2026-08-30** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -18,12 +18,12 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.65.38 | 268.40 ms | 267.52 ms | 2.408 ms | -| NUnit | 4.6.1 | 498.74 ms | 496.12 ms | 9.573 ms | -| MSTest | 4.3.3 | 490.02 ms | 489.68 ms | 11.852 ms | -| xUnit3 | 4.0.0 | 586.26 ms | 585.77 ms | 7.853 ms | -| **TUnit (AOT)** | 1.65.38 | 13.98 ms | 13.82 ms | 0.551 ms | -| xUnit3_AOT | 4.0.0 | 16.70 ms | 16.80 ms | 0.338 ms | +| **TUnit** | 1.65.68 | 281.32 ms | 278.96 ms | 11.233 ms | +| NUnit | 4.6.1 | 564.15 ms | 561.05 ms | 16.735 ms | +| MSTest | 4.3.3 | 507.18 ms | 505.65 ms | 12.262 ms | +| xUnit3 | 4.0.0 | 655.45 ms | 655.21 ms | 27.750 ms | +| **TUnit (AOT)** | 1.65.68 | 16.57 ms | 16.26 ms | 1.104 ms | +| xUnit3_AOT | 4.0.0 | 20.29 ms | 20.48 ms | 1.727 ms | ## 📈 Visual Comparison @@ -61,8 +61,8 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI xychart-beta title "DataDrivenTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT", "xUnit3_AOT"] - y-axis "Time (ms)" 0 --> 704 - bar [268.4, 498.74, 490.02, 586.26, 13.98, 16.7] + y-axis "Time (ms)" 0 --> 787 + bar [281.32, 564.15, 507.18, 655.45, 16.57, 20.29] ``` ## 🎯 Key Insights @@ -75,4 +75,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3, xUnit View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2026-08-23T00:20:42.432Z* +*Last generated: 2026-08-30T00:32:59.981Z* diff --git a/docs/docs/benchmarks/engine/MassiveParallelTests.md b/docs/docs/benchmarks/engine/MassiveParallelTests.md index 4485192783b..9bb1890ae93 100644 --- a/docs/docs/benchmarks/engine/MassiveParallelTests.md +++ b/docs/docs/benchmarks/engine/MassiveParallelTests.md @@ -9,7 +9,7 @@ sidebar_position: 5 > Parallel execution stress tests :::info Last Updated -This benchmark was automatically generated on **2026-08-23** from the latest CI run. +This benchmark was automatically generated on **2026-08-30** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -18,12 +18,12 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.65.38 | 471.8 ms | 461.6 ms | 18.81 ms | -| NUnit | 4.6.1 | 1,083.0 ms | 1,079.8 ms | 13.12 ms | -| MSTest | 4.3.3 | 2,975.1 ms | 2,974.6 ms | 14.65 ms | -| xUnit3 | 4.0.0 | 1,289.6 ms | 1,278.2 ms | 23.16 ms | -| **TUnit (AOT)** | 1.65.38 | 218.2 ms | 218.1 ms | 1.08 ms | -| xUnit3_AOT | 4.0.0 | 673.0 ms | 672.6 ms | 1.97 ms | +| **TUnit** | 1.65.68 | 535.8 ms | 538.1 ms | 23.04 ms | +| NUnit | 4.6.1 | 1,317.5 ms | 1,310.7 ms | 34.72 ms | +| MSTest | 4.3.3 | 3,040.5 ms | 3,027.4 ms | 40.38 ms | +| xUnit3 | 4.0.0 | 1,337.9 ms | 1,332.9 ms | 45.41 ms | +| **TUnit (AOT)** | 1.65.68 | 220.9 ms | 221.1 ms | 0.61 ms | +| xUnit3_AOT | 4.0.0 | 676.7 ms | 677.0 ms | 1.49 ms | ## 📈 Visual Comparison @@ -61,8 +61,8 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI xychart-beta title "MassiveParallelTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT", "xUnit3_AOT"] - y-axis "Time (ms)" 0 --> 3571 - bar [471.8, 1083, 2975.1, 1289.6, 218.2, 673] + y-axis "Time (ms)" 0 --> 3649 + bar [535.8, 1317.5, 3040.5, 1337.9, 220.9, 676.7] ``` ## 🎯 Key Insights @@ -75,4 +75,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3, xUnit View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2026-08-23T00:20:42.432Z* +*Last generated: 2026-08-30T00:32:59.982Z* diff --git a/docs/docs/benchmarks/engine/MatrixTests.md b/docs/docs/benchmarks/engine/MatrixTests.md index b430473b6a5..0e70f25bcb5 100644 --- a/docs/docs/benchmarks/engine/MatrixTests.md +++ b/docs/docs/benchmarks/engine/MatrixTests.md @@ -9,7 +9,7 @@ sidebar_position: 6 > Combinatorial test generation and execution :::info Last Updated -This benchmark was automatically generated on **2026-08-23** from the latest CI run. +This benchmark was automatically generated on **2026-08-30** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -18,12 +18,12 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.65.38 | 377.3 ms | 374.3 ms | 13.53 ms | -| NUnit | 4.6.1 | 1,443.4 ms | 1,438.9 ms | 13.23 ms | -| MSTest | 4.3.3 | 1,532.2 ms | 1,528.3 ms | 24.49 ms | -| xUnit3 | 4.0.0 | 960.5 ms | 957.4 ms | 52.75 ms | -| **TUnit (AOT)** | 1.65.38 | 120.4 ms | 120.5 ms | 1.11 ms | -| xUnit3_AOT | 4.0.0 | 275.0 ms | 275.1 ms | 1.08 ms | +| **TUnit** | 1.65.68 | 364.9 ms | 365.0 ms | 1.92 ms | +| NUnit | 4.6.1 | 1,536.5 ms | 1,538.5 ms | 6.01 ms | +| MSTest | 4.3.3 | 1,497.2 ms | 1,495.6 ms | 8.02 ms | +| xUnit3 | 4.0.0 | 861.9 ms | 862.6 ms | 9.53 ms | +| **TUnit (AOT)** | 1.65.68 | 117.1 ms | 117.1 ms | 1.20 ms | +| xUnit3_AOT | 4.0.0 | 269.2 ms | 269.0 ms | 0.88 ms | ## 📈 Visual Comparison @@ -61,8 +61,8 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI xychart-beta title "MatrixTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT", "xUnit3_AOT"] - y-axis "Time (ms)" 0 --> 1839 - bar [377.3, 1443.4, 1532.2, 960.5, 120.4, 275] + y-axis "Time (ms)" 0 --> 1844 + bar [364.9, 1536.5, 1497.2, 861.9, 117.1, 269.2] ``` ## 🎯 Key Insights @@ -75,4 +75,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3, xUnit View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2026-08-23T00:20:42.432Z* +*Last generated: 2026-08-30T00:32:59.982Z* diff --git a/docs/docs/benchmarks/engine/ScaleTests.md b/docs/docs/benchmarks/engine/ScaleTests.md index ef99fbe6779..42ee291670e 100644 --- a/docs/docs/benchmarks/engine/ScaleTests.md +++ b/docs/docs/benchmarks/engine/ScaleTests.md @@ -9,7 +9,7 @@ sidebar_position: 7 > Large test suites (150+ tests) measuring scalability :::info Last Updated -This benchmark was automatically generated on **2026-08-23** from the latest CI run. +This benchmark was automatically generated on **2026-08-30** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -18,12 +18,12 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.65.38 | 280.02 ms | 279.97 ms | 3.282 ms | -| NUnit | 4.6.1 | 522.04 ms | 515.34 ms | 22.927 ms | -| MSTest | 4.3.3 | 505.40 ms | 504.41 ms | 12.702 ms | -| xUnit3 | 4.0.0 | 620.62 ms | 618.35 ms | 15.701 ms | -| **TUnit (AOT)** | 1.65.38 | 19.86 ms | 20.26 ms | 2.114 ms | -| xUnit3_AOT | 4.0.0 | 23.07 ms | 22.85 ms | 0.874 ms | +| **TUnit** | 1.65.68 | 334.54 ms | 334.15 ms | 24.989 ms | +| NUnit | 4.6.1 | 643.81 ms | 636.45 ms | 32.013 ms | +| MSTest | 4.3.3 | 562.39 ms | 560.91 ms | 32.264 ms | +| xUnit3 | 4.0.0 | 710.91 ms | 708.11 ms | 30.502 ms | +| **TUnit (AOT)** | 1.65.68 | 18.88 ms | 18.84 ms | 0.550 ms | +| xUnit3_AOT | 4.0.0 | 23.38 ms | 23.59 ms | 1.044 ms | ## 📈 Visual Comparison @@ -61,8 +61,8 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI xychart-beta title "ScaleTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT", "xUnit3_AOT"] - y-axis "Time (ms)" 0 --> 745 - bar [280.02, 522.04, 505.4, 620.62, 19.86, 23.07] + y-axis "Time (ms)" 0 --> 854 + bar [334.54, 643.81, 562.39, 710.91, 18.88, 23.38] ``` ## 🎯 Key Insights @@ -75,4 +75,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3, xUnit View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2026-08-23T00:20:42.433Z* +*Last generated: 2026-08-30T00:32:59.982Z* diff --git a/docs/docs/benchmarks/engine/SetupTeardownTests.md b/docs/docs/benchmarks/engine/SetupTeardownTests.md index 68c88343dbc..f2aae685f29 100644 --- a/docs/docs/benchmarks/engine/SetupTeardownTests.md +++ b/docs/docs/benchmarks/engine/SetupTeardownTests.md @@ -9,7 +9,7 @@ sidebar_position: 8 > Expensive test fixtures with setup/teardown overhead :::info Last Updated -This benchmark was automatically generated on **2026-08-23** from the latest CI run. +This benchmark was automatically generated on **2026-08-30** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -18,12 +18,12 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.65.38 | 389.40 ms | 380.76 ms | 37.638 ms | -| NUnit | 4.6.1 | 1,090.23 ms | 1,076.47 ms | 46.892 ms | -| MSTest | 4.3.3 | 1,163.10 ms | 1,135.41 ms | 60.412 ms | -| xUnit3 | 4.0.0 | 784.00 ms | 785.63 ms | 20.951 ms | -| **TUnit (AOT)** | 1.65.38 | 70.18 ms | 69.89 ms | 2.527 ms | -| xUnit3_AOT | 4.0.0 | 179.90 ms | 179.56 ms | 3.249 ms | +| **TUnit** | 1.65.68 | 367.81 ms | 364.64 ms | 14.446 ms | +| NUnit | 4.6.1 | 1,270.05 ms | 1,267.62 ms | 52.049 ms | +| MSTest | 4.3.3 | 1,322.57 ms | 1,319.06 ms | 23.266 ms | +| xUnit3 | 4.0.0 | 955.39 ms | 959.77 ms | 26.122 ms | +| **TUnit (AOT)** | 1.65.68 | 75.64 ms | 75.74 ms | 1.348 ms | +| xUnit3_AOT | 4.0.0 | 182.31 ms | 182.43 ms | 1.503 ms | ## 📈 Visual Comparison @@ -61,8 +61,8 @@ This benchmark was automatically generated on **2026-08-23** from the latest CI xychart-beta title "SetupTeardownTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT", "xUnit3_AOT"] - y-axis "Time (ms)" 0 --> 1396 - bar [389.4, 1090.23, 1163.1, 784, 70.18, 179.9] + y-axis "Time (ms)" 0 --> 1588 + bar [367.81, 1270.05, 1322.57, 955.39, 75.64, 182.31] ``` ## 🎯 Key Insights @@ -75,4 +75,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3, xUnit View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2026-08-23T00:20:42.433Z* +*Last generated: 2026-08-30T00:32:59.983Z* diff --git a/docs/docs/benchmarks/index.md b/docs/docs/benchmarks/index.md index dd94279341d..c975c6a4cd1 100644 --- a/docs/docs/benchmarks/index.md +++ b/docs/docs/benchmarks/index.md @@ -7,7 +7,7 @@ sidebar_position: 1 # Performance Benchmarks :::info Last Updated -These benchmarks were automatically generated on **2026-08-23** from the latest CI run. +These benchmarks were automatically generated on **2026-08-30** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -37,7 +37,7 @@ These benchmarks compare TUnit against the most popular .NET testing frameworks: | Framework | Version Tested | |-----------|----------------| -| **TUnit** | 1.65.38 | +| **TUnit** | 1.65.68 | | **xUnit v3** | 4.0.0 | | **NUnit** | 4.6.1 | | **MSTest** | 4.3.3 | @@ -80,4 +80,4 @@ These benchmarks run automatically daily via [GitHub Actions](https://github.com Each benchmark runs multiple iterations with statistical analysis to ensure accuracy. Results may vary based on hardware and test characteristics. ::: -*Last generated: 2026-08-23T00:20:42.434Z* +*Last generated: 2026-08-30T00:32:59.983Z* diff --git a/docs/docs/benchmarks/methodology.md b/docs/docs/benchmarks/methodology.md index 3907dd66444..720cdc7b1ff 100644 --- a/docs/docs/benchmarks/methodology.md +++ b/docs/docs/benchmarks/methodology.md @@ -4,7 +4,6 @@ description: How TUnit's performance benchmarks are measured and compared sidebar_position: 2 --- - # Benchmark Methodology @@ -41,7 +40,6 @@ All benchmarks use [BenchmarkDotNet](https://benchmarkdotnet.org/), the industry **Purpose**: Measure parameterized test performance **What's tested**: - ```csharp [Test] [Arguments(1, 2, 3)] @@ -61,7 +59,6 @@ public async Task TestAddition(int a, int b, int expected) **Purpose**: Measure async/await pattern performance **What's tested**: - ```csharp [Test] public async Task TestAsyncOperation() @@ -91,7 +88,6 @@ public async Task TestAsyncOperation() **Purpose**: Measure combinatorial test generation **What's tested**: - ```csharp [Test] [MatrixDataSource] @@ -160,7 +156,6 @@ dotnet build -c Release -p:TestFramework=MSTEST ``` ### 2. Execution Phase - ```csharp using BenchmarkDotNet.Attributes; diff --git a/docs/docs/benchmarks/mocks/Callback.md b/docs/docs/benchmarks/mocks/Callback.md index b67b1a9998f..b69b63b75e8 100644 --- a/docs/docs/benchmarks/mocks/Callback.md +++ b/docs/docs/benchmarks/mocks/Callback.md @@ -9,7 +9,7 @@ sidebar_position: 2 > Callback registration and execution — comparing **TUnit.Mocks** (source-generated) against runtime proxy-based mocking libraries. :::info Last Updated -This benchmark was automatically generated on **2026-08-26** from the latest CI run. +This benchmark was automatically generated on **2026-09-04** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -20,12 +20,12 @@ Callback registration and execution: | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 524.5 ns | 4.74 ns | 3.96 ns | 3.11 KB | -| Imposter | 377.2 ns | 4.79 ns | 4.48 ns | 2.66 KB | -| Mockolate | 274.2 ns | 3.07 ns | 2.87 ns | 1.8 KB | -| Moq | 108,008.1 ns | 672.13 ns | 561.26 ns | 13.29 KB | -| NSubstitute | 3,564.6 ns | 55.53 ns | 51.95 ns | 7.85 KB | -| FakeItEasy | 3,801.3 ns | 47.95 ns | 44.85 ns | 7.44 KB | +| **TUnit.Mocks** | 525.9 ns | 10.29 ns | 10.10 ns | 3.11 KB | +| Imposter | 356.3 ns | 2.99 ns | 2.79 ns | 2.66 KB | +| Mockolate | 277.9 ns | 3.23 ns | 3.02 ns | 1.8 KB | +| Moq | 107,115.3 ns | 524.39 ns | 490.52 ns | 13.29 KB | +| NSubstitute | 3,576.5 ns | 59.01 ns | 52.32 ns | 7.85 KB | +| FakeItEasy | 3,780.5 ns | 28.35 ns | 23.67 ns | 7.44 KB | ```mermaid %%{init: { @@ -51,8 +51,8 @@ Callback registration and execution: xychart-beta title "Callback Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 129610 - bar [524.5, 377.2, 274.2, 108008.1, 3564.6, 3801.3] + y-axis "Time (ns)" 0 --> 128539 + bar [525.9, 356.3, 277.9, 107115.3, 3576.5, 3780.5] ``` --- @@ -61,12 +61,12 @@ xychart-beta | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 626.7 ns | 7.50 ns | 6.65 ns | 3.2 KB | -| Imposter | 433.0 ns | 1.87 ns | 1.66 ns | 2.82 KB | -| Mockolate | 312.6 ns | 3.49 ns | 3.27 ns | 1.84 KB | -| Moq | 114,029.5 ns | 664.83 ns | 589.36 ns | 13.76 KB | -| NSubstitute | 3,972.7 ns | 74.00 ns | 69.22 ns | 8.41 KB | -| FakeItEasy | 4,608.1 ns | 91.72 ns | 94.19 ns | 9.26 KB | +| **TUnit.Mocks** | 600.6 ns | 3.98 ns | 3.53 ns | 3.2 KB | +| Imposter | 434.1 ns | 2.38 ns | 2.11 ns | 2.82 KB | +| Mockolate | 305.6 ns | 2.68 ns | 2.51 ns | 1.84 KB | +| Moq | 114,961.5 ns | 454.49 ns | 402.89 ns | 13.76 KB | +| NSubstitute | 3,942.0 ns | 37.84 ns | 35.40 ns | 8.41 KB | +| FakeItEasy | 4,552.1 ns | 61.89 ns | 54.86 ns | 9.26 KB | ```mermaid %%{init: { @@ -92,8 +92,8 @@ xychart-beta xychart-beta title "Callback (with args) Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 136836 - bar [626.7, 433, 312.6, 114029.5, 3972.7, 4608.1] + y-axis "Time (ns)" 0 --> 137954 + bar [600.6, 434.1, 305.6, 114961.5, 3942, 4552.1] ``` ## 🎯 Key Insights @@ -106,4 +106,4 @@ This benchmark compares **TUnit.Mocks** (source-generated) against runtime proxy View the [mock benchmarks overview](/docs/benchmarks/mocks) for methodology details and environment information. ::: -*Last generated: 2026-08-26T02:57:20.474Z* +*Last generated: 2026-09-04T02:33:16.366Z* diff --git a/docs/docs/benchmarks/mocks/CombinedWorkflow.md b/docs/docs/benchmarks/mocks/CombinedWorkflow.md index 318de9e6dfe..497bc3d22dd 100644 --- a/docs/docs/benchmarks/mocks/CombinedWorkflow.md +++ b/docs/docs/benchmarks/mocks/CombinedWorkflow.md @@ -9,7 +9,7 @@ sidebar_position: 3 > Full workflow: create → setup → invoke → verify — comparing **TUnit.Mocks** (source-generated) against runtime proxy-based mocking libraries. :::info Last Updated -This benchmark was automatically generated on **2026-08-26** from the latest CI run. +This benchmark was automatically generated on **2026-09-04** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -20,12 +20,12 @@ Full workflow: create → setup → invoke → verify: | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 2.005 μs | 0.0324 μs | 0.0287 μs | 6.23 KB | -| Imposter | 2.783 μs | 0.0556 μs | 0.1002 μs | 15.71 KB | -| Mockolate | 1.790 μs | 0.0342 μs | 0.0380 μs | 7.36 KB | -| Moq | 303.325 μs | 3.7601 μs | 3.3332 μs | 36.3 KB | -| NSubstitute | 18.320 μs | 0.1262 μs | 0.1180 μs | 26.72 KB | -| FakeItEasy | 16.434 μs | 0.2877 μs | 0.2550 μs | 25.52 KB | +| **TUnit.Mocks** | 1.890 μs | 0.0200 μs | 0.0187 μs | 6.23 KB | +| Imposter | 2.892 μs | 0.0578 μs | 0.0540 μs | 15.71 KB | +| Mockolate | 1.680 μs | 0.0194 μs | 0.0172 μs | 7.36 KB | +| Moq | 404.656 μs | 2.0986 μs | 1.8603 μs | 36.49 KB | +| NSubstitute | 19.260 μs | 0.0823 μs | 0.0770 μs | 26.72 KB | +| FakeItEasy | 19.347 μs | 0.1678 μs | 0.1488 μs | 25.85 KB | ```mermaid %%{init: { @@ -51,8 +51,8 @@ Full workflow: create → setup → invoke → verify: xychart-beta title "CombinedWorkflow Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (μs)" 0 --> 364 - bar [2.005, 2.783, 1.79, 303.325, 18.32, 16.434] + y-axis "Time (μs)" 0 --> 486 + bar [1.89, 2.892, 1.68, 404.656, 19.26, 19.347] ``` ## 🎯 Key Insights @@ -65,4 +65,4 @@ This benchmark compares **TUnit.Mocks** (source-generated) against runtime proxy View the [mock benchmarks overview](/docs/benchmarks/mocks) for methodology details and environment information. ::: -*Last generated: 2026-08-26T02:57:20.474Z* +*Last generated: 2026-09-04T02:33:16.366Z* diff --git a/docs/docs/benchmarks/mocks/Invocation.md b/docs/docs/benchmarks/mocks/Invocation.md index 601398ba407..c89f4913df7 100644 --- a/docs/docs/benchmarks/mocks/Invocation.md +++ b/docs/docs/benchmarks/mocks/Invocation.md @@ -9,7 +9,7 @@ sidebar_position: 4 > Calling methods on mock objects — comparing **TUnit.Mocks** (source-generated) against runtime proxy-based mocking libraries. :::info Last Updated -This benchmark was automatically generated on **2026-08-26** from the latest CI run. +This benchmark was automatically generated on **2026-09-04** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -20,12 +20,12 @@ Calling methods on mock objects: | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 276.9 ns | 69.53 ns | 3.81 ns | 128 B | -| Imposter | 298.5 ns | 69.70 ns | 3.82 ns | 168 B | -| Mockolate | 111.1 ns | 16.94 ns | 0.93 ns | 84 B | -| Moq | 810.2 ns | 395.18 ns | 21.66 ns | 376 B | -| NSubstitute | 749.4 ns | 613.64 ns | 33.64 ns | 304 B | -| FakeItEasy | 1,833.6 ns | 350.91 ns | 19.23 ns | 944 B | +| **TUnit.Mocks** | 276.11 ns | 61.98 ns | 3.397 ns | 128 B | +| Imposter | 303.36 ns | 87.10 ns | 4.774 ns | 168 B | +| Mockolate | 120.36 ns | 50.28 ns | 2.756 ns | 84 B | +| Moq | 813.27 ns | 76.74 ns | 4.206 ns | 376 B | +| NSubstitute | 710.74 ns | 172.33 ns | 9.446 ns | 304 B | +| FakeItEasy | 1,738.63 ns | 161.03 ns | 8.826 ns | 944 B | ```mermaid %%{init: { @@ -51,8 +51,8 @@ Calling methods on mock objects: xychart-beta title "Invocation Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 2201 - bar [276.9, 298.5, 111.1, 810.2, 749.4, 1833.6] + y-axis "Time (ns)" 0 --> 2087 + bar [276.11, 303.36, 120.36, 813.27, 710.74, 1738.63] ``` --- @@ -61,12 +61,12 @@ xychart-beta | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 166.9 ns | 87.98 ns | 4.82 ns | 96 B | -| Imposter | 303.1 ns | 55.19 ns | 3.03 ns | 168 B | -| Mockolate | 100.8 ns | 71.99 ns | 3.95 ns | 60 B | -| Moq | 564.4 ns | 298.13 ns | 16.34 ns | 296 B | -| NSubstitute | 656.3 ns | 101.12 ns | 5.54 ns | 328 B | -| FakeItEasy | 1,623.5 ns | 304.96 ns | 16.72 ns | 776 B | +| **TUnit.Mocks** | 167.11 ns | 74.09 ns | 4.061 ns | 96 B | +| Imposter | 291.19 ns | 92.03 ns | 5.045 ns | 168 B | +| Mockolate | 93.41 ns | 22.35 ns | 1.225 ns | 60 B | +| Moq | 532.70 ns | 81.24 ns | 4.453 ns | 296 B | +| NSubstitute | 602.82 ns | 102.70 ns | 5.629 ns | 272 B | +| FakeItEasy | 1,545.15 ns | 591.79 ns | 32.438 ns | 776 B | ```mermaid %%{init: { @@ -92,8 +92,8 @@ xychart-beta xychart-beta title "Invocation (String) Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 1949 - bar [166.9, 303.1, 100.8, 564.4, 656.3, 1623.5] + y-axis "Time (ns)" 0 --> 1855 + bar [167.11, 291.19, 93.41, 532.7, 602.82, 1545.15] ``` --- @@ -102,12 +102,12 @@ xychart-beta | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 27,342.5 ns | 10,466.71 ns | 573.72 ns | 12736 B | -| Imposter | 29,495.1 ns | 10,705.06 ns | 586.78 ns | 16800 B | -| Mockolate | 10,825.3 ns | 2,802.84 ns | 153.63 ns | 8400 B | -| Moq | 83,855.2 ns | 24,978.27 ns | 1,369.14 ns | 37600 B | -| NSubstitute | 81,404.3 ns | 34,696.37 ns | 1,901.82 ns | 36448 B | -| FakeItEasy | 182,322.3 ns | 55,668.73 ns | 3,051.39 ns | 94400 B | +| **TUnit.Mocks** | 27,240.28 ns | 9,886.83 ns | 541.931 ns | 12736 B | +| Imposter | 29,050.40 ns | 6,147.02 ns | 336.939 ns | 16800 B | +| Mockolate | 10,561.82 ns | 4,525.93 ns | 248.081 ns | 8400 B | +| Moq | 79,428.50 ns | 6,454.62 ns | 353.799 ns | 37600 B | +| NSubstitute | 70,130.53 ns | 9,730.73 ns | 533.374 ns | 30848 B | +| FakeItEasy | 173,430.43 ns | 34,832.13 ns | 1,909.267 ns | 94400 B | ```mermaid %%{init: { @@ -133,8 +133,8 @@ xychart-beta xychart-beta title "Invocation (100 calls) Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 218787 - bar [27342.5, 29495.1, 10825.3, 83855.2, 81404.3, 182322.3] + y-axis "Time (ns)" 0 --> 208117 + bar [27240.28, 29050.4, 10561.82, 79428.5, 70130.53, 173430.43] ``` ## 🎯 Key Insights @@ -147,4 +147,4 @@ This benchmark compares **TUnit.Mocks** (source-generated) against runtime proxy View the [mock benchmarks overview](/docs/benchmarks/mocks) for methodology details and environment information. ::: -*Last generated: 2026-08-26T02:57:20.474Z* +*Last generated: 2026-09-04T02:33:16.366Z* diff --git a/docs/docs/benchmarks/mocks/MockCreation.md b/docs/docs/benchmarks/mocks/MockCreation.md index 4c0dc745150..d7d363e1a32 100644 --- a/docs/docs/benchmarks/mocks/MockCreation.md +++ b/docs/docs/benchmarks/mocks/MockCreation.md @@ -9,7 +9,7 @@ sidebar_position: 5 > Mock instance creation performance — comparing **TUnit.Mocks** (source-generated) against runtime proxy-based mocking libraries. :::info Last Updated -This benchmark was automatically generated on **2026-08-26** from the latest CI run. +This benchmark was automatically generated on **2026-09-04** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -20,12 +20,12 @@ Mock instance creation performance: | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 15.770 ns | 0.2187 ns | 0.2045 ns | 200 B | -| Imposter | 52.184 ns | 0.4922 ns | 0.4363 ns | 440 B | -| Mockolate | 9.503 ns | 0.2236 ns | 0.2486 ns | 160 B | -| Moq | 745.059 ns | 10.3641 ns | 9.6946 ns | 2048 B | -| NSubstitute | 937.076 ns | 11.4854 ns | 10.7434 ns | 5000 B | -| FakeItEasy | 995.678 ns | 13.9343 ns | 13.0341 ns | 2714 B | +| **TUnit.Mocks** | 23.40 ns | 0.224 ns | 0.199 ns | 200 B | +| Imposter | 79.96 ns | 0.386 ns | 0.342 ns | 440 B | +| Mockolate | 13.99 ns | 0.094 ns | 0.088 ns | 160 B | +| Moq | 1,010.23 ns | 15.006 ns | 14.036 ns | 2048 B | +| NSubstitute | 1,452.00 ns | 14.914 ns | 13.950 ns | 5000 B | +| FakeItEasy | 1,445.48 ns | 28.528 ns | 51.442 ns | 2715 B | ```mermaid %%{init: { @@ -51,8 +51,8 @@ Mock instance creation performance: xychart-beta title "MockCreation Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 1195 - bar [15.77, 52.184, 9.503, 745.059, 937.076, 995.678] + y-axis "Time (ns)" 0 --> 1743 + bar [23.4, 79.96, 13.99, 1010.23, 1452, 1445.48] ``` --- @@ -61,12 +61,12 @@ xychart-beta | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 16.442 ns | 0.2587 ns | 0.2420 ns | 200 B | -| Imposter | 81.616 ns | 0.7949 ns | 0.7046 ns | 696 B | -| Mockolate | 9.639 ns | 0.1504 ns | 0.1407 ns | 176 B | -| Moq | 694.943 ns | 13.4170 ns | 13.7783 ns | 1912 B | -| NSubstitute | 935.775 ns | 7.6325 ns | 7.1394 ns | 5000 B | -| FakeItEasy | 1,006.596 ns | 7.6581 ns | 6.3949 ns | 2714 B | +| **TUnit.Mocks** | 23.46 ns | 0.239 ns | 0.223 ns | 200 B | +| Imposter | 123.02 ns | 0.805 ns | 0.753 ns | 696 B | +| Mockolate | 14.15 ns | 0.129 ns | 0.115 ns | 176 B | +| Moq | 959.91 ns | 9.791 ns | 9.159 ns | 1912 B | +| NSubstitute | 1,385.63 ns | 27.592 ns | 35.877 ns | 5000 B | +| FakeItEasy | 1,287.78 ns | 23.041 ns | 20.425 ns | 2715 B | ```mermaid %%{init: { @@ -92,8 +92,8 @@ xychart-beta xychart-beta title "MockCreation (Repository) Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 1208 - bar [16.442, 81.616, 9.639, 694.943, 935.775, 1006.596] + y-axis "Time (ns)" 0 --> 1663 + bar [23.46, 123.02, 14.15, 959.91, 1385.63, 1287.78] ``` ## 🎯 Key Insights @@ -106,4 +106,4 @@ This benchmark compares **TUnit.Mocks** (source-generated) against runtime proxy View the [mock benchmarks overview](/docs/benchmarks/mocks) for methodology details and environment information. ::: -*Last generated: 2026-08-26T02:57:20.474Z* +*Last generated: 2026-09-04T02:33:16.366Z* diff --git a/docs/docs/benchmarks/mocks/Setup.md b/docs/docs/benchmarks/mocks/Setup.md index a90e10c49f3..363f9b9ec98 100644 --- a/docs/docs/benchmarks/mocks/Setup.md +++ b/docs/docs/benchmarks/mocks/Setup.md @@ -9,7 +9,7 @@ sidebar_position: 6 > Mock behavior configuration (returns, matchers) — comparing **TUnit.Mocks** (source-generated) against runtime proxy-based mocking libraries. :::info Last Updated -This benchmark was automatically generated on **2026-08-26** from the latest CI run. +This benchmark was automatically generated on **2026-09-04** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -20,12 +20,12 @@ Mock behavior configuration (returns, matchers): | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 551.7 ns | 10.96 ns | 21.89 ns | 2.34 KB | -| Imposter | 851.7 ns | 16.20 ns | 24.25 ns | 6.12 KB | -| Mockolate | 331.7 ns | 6.55 ns | 10.19 ns | 1.41 KB | -| Moq | 433,988.1 ns | 3,886.68 ns | 3,635.60 ns | 28.68 KB | -| NSubstitute | 6,263.8 ns | 76.62 ns | 63.98 ns | 9.01 KB | -| FakeItEasy | 8,319.2 ns | 152.28 ns | 142.44 ns | 10.45 KB | +| **TUnit.Mocks** | 421.5 ns | 7.07 ns | 5.90 ns | 2.34 KB | +| Imposter | 665.9 ns | 13.34 ns | 25.05 ns | 6.12 KB | +| Mockolate | 255.6 ns | 4.02 ns | 3.36 ns | 1.41 KB | +| Moq | 159,727.2 ns | 2,598.80 ns | 2,552.37 ns | 28.61 KB | +| NSubstitute | 4,769.0 ns | 93.76 ns | 87.71 ns | 9.01 KB | +| FakeItEasy | 4,569.9 ns | 88.62 ns | 118.30 ns | 10.44 KB | ```mermaid %%{init: { @@ -51,8 +51,8 @@ Mock behavior configuration (returns, matchers): xychart-beta title "Setup Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 520786 - bar [551.7, 851.7, 331.7, 433988.1, 6263.8, 8319.2] + y-axis "Time (ns)" 0 --> 191673 + bar [421.5, 665.9, 255.6, 159727.2, 4769, 4569.9] ``` --- @@ -61,12 +61,12 @@ xychart-beta | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 774.0 ns | 15.36 ns | 21.53 ns | 3.15 KB | -| Imposter | 1,444.0 ns | 22.32 ns | 18.63 ns | 10.59 KB | -| Mockolate | 548.7 ns | 10.31 ns | 9.64 ns | 2.35 KB | -| Moq | 114,312.6 ns | 812.13 ns | 719.93 ns | 16.53 KB | -| NSubstitute | 12,314.6 ns | 76.22 ns | 63.64 ns | 20.31 KB | -| FakeItEasy | 7,900.2 ns | 127.71 ns | 113.21 ns | 11.71 KB | +| **TUnit.Mocks** | 674.2 ns | 12.98 ns | 16.41 ns | 3.15 KB | +| Imposter | 1,087.0 ns | 21.54 ns | 28.75 ns | 10.59 KB | +| Mockolate | 440.9 ns | 6.52 ns | 5.78 ns | 2.35 KB | +| Moq | 42,070.2 ns | 453.37 ns | 378.58 ns | 16.52 KB | +| NSubstitute | 8,155.3 ns | 161.65 ns | 315.28 ns | 20.66 KB | +| FakeItEasy | 4,233.0 ns | 83.31 ns | 129.70 ns | 11.7 KB | ```mermaid %%{init: { @@ -92,8 +92,8 @@ xychart-beta xychart-beta title "Setup (Multiple) Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 137176 - bar [774, 1444, 548.7, 114312.6, 12314.6, 7900.2] + y-axis "Time (ns)" 0 --> 50485 + bar [674.2, 1087, 440.9, 42070.2, 8155.3, 4233] ``` ## 🎯 Key Insights @@ -106,4 +106,4 @@ This benchmark compares **TUnit.Mocks** (source-generated) against runtime proxy View the [mock benchmarks overview](/docs/benchmarks/mocks) for methodology details and environment information. ::: -*Last generated: 2026-08-26T02:57:20.474Z* +*Last generated: 2026-09-04T02:33:16.366Z* diff --git a/docs/docs/benchmarks/mocks/Verification.md b/docs/docs/benchmarks/mocks/Verification.md index 949c31438b8..76b6092134d 100644 --- a/docs/docs/benchmarks/mocks/Verification.md +++ b/docs/docs/benchmarks/mocks/Verification.md @@ -9,7 +9,7 @@ sidebar_position: 7 > Verifying mock method calls — comparing **TUnit.Mocks** (source-generated) against runtime proxy-based mocking libraries. :::info Last Updated -This benchmark was automatically generated on **2026-08-26** from the latest CI run. +This benchmark was automatically generated on **2026-09-04** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -20,12 +20,12 @@ Verifying mock method calls: | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 760.75 ns | 4.092 ns | 3.828 ns | 3008 B | -| Imposter | 680.80 ns | 5.407 ns | 4.793 ns | 4688 B | -| Mockolate | 398.57 ns | 0.992 ns | 0.829 ns | 2128 B | -| Moq | 240,480.10 ns | 1,310.717 ns | 1,161.917 ns | 24324 B | -| NSubstitute | 6,464.94 ns | 50.175 ns | 41.898 ns | 10064 B | -| FakeItEasy | 6,411.13 ns | 29.251 ns | 25.930 ns | 10722 B | +| **TUnit.Mocks** | 996.63 ns | 11.722 ns | 10.965 ns | 3008 B | +| Imposter | 1,029.90 ns | 14.239 ns | 12.622 ns | 4688 B | +| Mockolate | 582.88 ns | 7.604 ns | 7.113 ns | 2128 B | +| Moq | 256,197.99 ns | 1,808.975 ns | 1,603.609 ns | 24306 B | +| NSubstitute | 7,438.36 ns | 48.396 ns | 42.902 ns | 10064 B | +| FakeItEasy | 7,377.24 ns | 43.861 ns | 38.882 ns | 10731 B | ```mermaid %%{init: { @@ -51,8 +51,8 @@ Verifying mock method calls: xychart-beta title "Verification Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 288577 - bar [760.75, 680.8, 398.57, 240480.1, 6464.94, 6411.13] + y-axis "Time (ns)" 0 --> 307438 + bar [996.63, 1029.9, 582.88, 256197.99, 7438.36, 7377.24] ``` --- @@ -61,12 +61,12 @@ xychart-beta | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 55.52 ns | 0.206 ns | 0.183 ns | 320 B | -| Imposter | 335.09 ns | 0.901 ns | 0.753 ns | 2400 B | -| Mockolate | 243.15 ns | 0.491 ns | 0.435 ns | 1144 B | -| Moq | 61,824.78 ns | 234.600 ns | 195.902 ns | 6925 B | -| NSubstitute | 3,588.04 ns | 13.947 ns | 12.363 ns | 7088 B | -| FakeItEasy | 3,258.96 ns | 49.439 ns | 46.246 ns | 5210 B | +| **TUnit.Mocks** | 71.58 ns | 1.433 ns | 1.962 ns | 320 B | +| Imposter | 471.49 ns | 6.386 ns | 5.974 ns | 2400 B | +| Mockolate | 316.54 ns | 6.311 ns | 8.638 ns | 1144 B | +| Moq | 67,662.70 ns | 422.716 ns | 374.727 ns | 6925 B | +| NSubstitute | 3,982.31 ns | 27.547 ns | 25.767 ns | 7088 B | +| FakeItEasy | 3,817.50 ns | 34.006 ns | 30.145 ns | 5299 B | ```mermaid %%{init: { @@ -92,8 +92,8 @@ xychart-beta xychart-beta title "Verification (Never) Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 74190 - bar [55.52, 335.09, 243.15, 61824.78, 3588.04, 3258.96] + y-axis "Time (ns)" 0 --> 81196 + bar [71.58, 471.49, 316.54, 67662.7, 3982.31, 3817.5] ``` --- @@ -102,12 +102,12 @@ xychart-beta | Library | Mean | Error | StdDev | Allocated | |---------|------|-------|--------|-----------| -| **TUnit.Mocks** | 1,261.68 ns | 2.316 ns | 2.167 ns | 4472 B | -| Imposter | 1,660.58 ns | 5.565 ns | 4.933 ns | 11192 B | -| Mockolate | 1,137.61 ns | 3.246 ns | 3.036 ns | 5240 B | -| Moq | 350,973.61 ns | 2,881.199 ns | 2,695.076 ns | 34699 B | -| NSubstitute | 11,253.45 ns | 35.831 ns | 29.920 ns | 16762 B | -| FakeItEasy | 11,742.28 ns | 65.267 ns | 61.051 ns | 19344 B | +| **TUnit.Mocks** | 1,638.65 ns | 14.438 ns | 13.506 ns | 4472 B | +| Imposter | 2,347.77 ns | 46.412 ns | 78.812 ns | 11192 B | +| Mockolate | 1,414.07 ns | 24.072 ns | 22.517 ns | 5240 B | +| Moq | 356,518.29 ns | 2,143.733 ns | 1,900.364 ns | 34814 B | +| NSubstitute | 12,792.53 ns | 64.138 ns | 56.857 ns | 16762 B | +| FakeItEasy | 13,248.07 ns | 33.295 ns | 29.516 ns | 19238 B | ```mermaid %%{init: { @@ -133,8 +133,8 @@ xychart-beta xychart-beta title "Verification (Multiple) Performance Comparison" x-axis ["TUnit.Mocks", "Imposter", "Mockolate", "Moq", "NSubstitute", "FakeItEasy"] - y-axis "Time (ns)" 0 --> 421169 - bar [1261.68, 1660.58, 1137.61, 350973.61, 11253.45, 11742.28] + y-axis "Time (ns)" 0 --> 427822 + bar [1638.65, 2347.77, 1414.07, 356518.29, 12792.53, 13248.07] ``` ## 🎯 Key Insights @@ -147,4 +147,4 @@ This benchmark compares **TUnit.Mocks** (source-generated) against runtime proxy View the [mock benchmarks overview](/docs/benchmarks/mocks) for methodology details and environment information. ::: -*Last generated: 2026-08-26T02:57:20.474Z* +*Last generated: 2026-09-04T02:33:16.366Z* diff --git a/docs/docs/benchmarks/mocks/index.md b/docs/docs/benchmarks/mocks/index.md index 24cd9f8eb78..60d01b85118 100644 --- a/docs/docs/benchmarks/mocks/index.md +++ b/docs/docs/benchmarks/mocks/index.md @@ -7,7 +7,7 @@ sidebar_position: 4 # Mock Library Benchmarks :::info Last Updated -These benchmarks were automatically generated on **2026-08-26** from the latest CI run. +These benchmarks were automatically generated on **2026-09-04** from the latest CI run. **Environment:** Ubuntu Latest • .NET SDK 10.0.400 ::: @@ -51,7 +51,7 @@ Each benchmark category tests a specific aspect of mocking library usage: - **Tool**: BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat) - **OS**: Ubuntu Latest (GitHub Actions) -- **Runtime**: .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3 +- **Runtime**: .NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4 - **Statistical Rigor**: Multiple iterations with warm-up and outlier detection - **Memory**: Allocation tracking enabled via `[MemoryDiagnoser]` @@ -76,4 +76,4 @@ These benchmarks run automatically daily via [GitHub Actions](https://github.com Each benchmark runs multiple iterations with statistical analysis to ensure accuracy. Results may vary based on hardware and test characteristics. ::: -*Last generated: 2026-08-26T02:57:20.474Z* +*Last generated: 2026-09-04T02:33:16.366Z* diff --git a/docs/docs/comparison/framework-differences.md b/docs/docs/comparison/framework-differences.md index 838f84a579a..d7091d8a2e8 100644 --- a/docs/docs/comparison/framework-differences.md +++ b/docs/docs/comparison/framework-differences.md @@ -1,4 +1,3 @@ - # Framework Differences @@ -45,11 +44,11 @@ In TUnit, you can inject a `TestContext` into your teardown method, or call `Tes xUnit assertions have the classic problem of unclear argument order: - ```csharp -var one = 2; -Assert.Equal(1, one); // is 1 the expected or actual? -Assert.Equal(one, 1); // ...or is it this way round? +var one = 1; +var anotherOne = 1; +Xunit.Assert.Equal(one, anotherOne); // which variable is expected? +Xunit.Assert.Equal(anotherOne, one); // ...or is it this way round? ``` TUnit uses a fluent syntax that reads naturally: `await Assert.That(one).IsEqualTo(1);` @@ -98,7 +97,6 @@ In other frameworks, running tests in a specific order usually means disabling p TUnit has `[DependsOn(...)]` — a test waits for its dependencies to finish, without disabling parallelism for everything else: - ```csharp [Test] public async Task Test1() { ... } diff --git a/docs/docs/examples/aspire.md b/docs/docs/examples/aspire.md index 3962b0d8393..ad72ddc13a2 100644 --- a/docs/docs/examples/aspire.md +++ b/docs/docs/examples/aspire.md @@ -1,4 +1,4 @@ - + # Aspire Integration Testing @@ -127,10 +127,10 @@ Use `Shared = SharedType.PerTestSession` to start the Aspire app once and share ```csharp [ClassDataSource(Shared = SharedType.PerTestSession)] -public class OrderTests(AppFixture fixture) { /* ... */ } +public class OrderTests(AppFixture fixture) { private AppFixture Fixture { get; } = fixture; } [ClassDataSource(Shared = SharedType.PerTestSession)] -public class ProductTests(AppFixture fixture) { /* ... */ } +public class ProductTests(AppFixture fixture) { private AppFixture Fixture { get; } = fixture; } // Both test classes share the same AppFixture instance ``` @@ -141,16 +141,19 @@ This is the recommended approach since starting an Aspire distributed applicatio By default, the fixture waits for **all resources to become healthy** before tests run. You can customize this: ```csharp -public class AppFixture : AspireFixture +public class AllRunningAppFixture : AspireFixture { - // Option 1: Change the wait behavior via property protected override ResourceWaitBehavior WaitBehavior => ResourceWaitBehavior.AllRunning; +} - // Option 2: Wait for specific resources only +public class NamedResourcesAppFixture : AspireFixture +{ protected override ResourceWaitBehavior WaitBehavior => ResourceWaitBehavior.Named; protected override IEnumerable ResourcesToWaitFor() => ["apiservice", "worker"]; +} - // Option 3: Full control over the waiting logic +public class CustomWaitAppFixture : AspireFixture +{ protected override async Task WaitForResourcesAsync( DistributedApplication app, CancellationToken cancellationToken) { @@ -464,7 +467,8 @@ public class RedisFixture : IAsyncInitializer, IAsyncDisposable public async Task InitializeAsync() { - var connectionString = await App.GetConnectionStringAsync("redis"); + var connectionString = await App.GetConnectionStringAsync("redis") + ?? throw new InvalidOperationException("Redis did not provide a connection string."); Connection = await ConnectionMultiplexer.ConnectAsync(connectionString); } @@ -624,7 +628,7 @@ Use `App` to access the full `DistributedApplication`, then get services or conn var notifications = fixture.App.Services.GetRequiredService(); // Connection strings -var connStr = await fixture.GetConnectionStringAsync("postgresdb"); +var connStr = await fixture.GetConnectionStringAsync("postgresdb") ?? throw new InvalidOperationException("Missing postgresdb connection string"); ``` ### Can I run different AppHosts in different test classes? @@ -636,10 +640,10 @@ public class AppAFixture : AspireFixture { } public class AppBFixture : AspireFixture { } [ClassDataSource(Shared = SharedType.PerTestSession)] -public class AppATests(AppAFixture fixture) { /* ... */ } +public class AppATests(AppAFixture fixture) { private AppAFixture Fixture { get; } = fixture; } [ClassDataSource(Shared = SharedType.PerTestSession)] -public class AppBTests(AppBFixture fixture) { /* ... */ } +public class AppBTests(AppBFixture fixture) { private AppBFixture Fixture { get; } = fixture; } ``` ### How do I skip waiting for tool containers? @@ -678,7 +682,10 @@ If a resource stays in `Running` but never reaches `Healthy`, check: If the resource doesn't have health checks, use `AllRunning` instead of `AllHealthy`: ```csharp -protected override ResourceWaitBehavior WaitBehavior => ResourceWaitBehavior.AllRunning; +public class RunningResourcesAppFixture : AspireFixture +{ + protected override ResourceWaitBehavior WaitBehavior => ResourceWaitBehavior.AllRunning; +} ``` ### What's the difference between TUnit.Aspire and TUnit.AspNetCore? diff --git a/docs/docs/examples/aspnet.md b/docs/docs/examples/aspnet.md index 7dd8c97b653..093481194dd 100644 --- a/docs/docs/examples/aspnet.md +++ b/docs/docs/examples/aspnet.md @@ -1,4 +1,3 @@ - # ASP.NET Core Integration Testing @@ -301,7 +300,7 @@ var dotPrefix = GetIsolatedPrefix("."); // Returns "test.42." ```csharp public class InMemoryDatabase : IAsyncInitializer, IAsyncDisposable { - public PostgreSqlContainer Container { get; } = new PostgreSqlBuilder() + public PostgreSqlContainer Container { get; } = new PostgreSqlBuilder("postgres:18") .WithImage("postgres:16-alpine") .Build(); @@ -375,7 +374,9 @@ public class TodoDbContext : DbContext // IConfiguration is optional: resolved via DI in the app, absent when // constructing standalone (e.g. in SetupAsync for EnsureCreatedAsync). - public TodoDbContext(DbContextOptions options, IConfiguration? config = null) + public TodoDbContext( + DbContextOptions options, + Microsoft.Extensions.Configuration.IConfiguration? config = null) : base(options) { SchemaName = config?["Database:Schema"] ?? "public"; @@ -469,10 +470,10 @@ Capture and inspect HTTP requests/responses for assertions: ```csharp public class CaptureTests : TestsBase { - protected override WebApplicationTestOptions Options => new() + protected override void ConfigureTestOptions(WebApplicationTestOptions options) { - EnableHttpExchangeCapture = true - }; + options.EnableHttpExchangeCapture = true; + } [Test] public async Task RequestIsCaptured() @@ -500,10 +501,10 @@ using TUnit.AspNetCore.Interception; public class CaptureTests : TestsBase { - protected override WebApplicationTestOptions Options => new() + protected override void ConfigureTestOptions(WebApplicationTestOptions options) { - EnableHttpExchangeCapture = true - }; + options.EnableHttpExchangeCapture = true; + } protected override void ConfigureTestServices(IServiceCollection services) { @@ -604,10 +605,10 @@ dotnet add package TUnit.Logging.Microsoft using TUnit.Logging.Microsoft; // Via ILoggingBuilder -builder.Logging.AddTUnit(TestContext.Current!); +TUnit.Logging.Microsoft.LoggingBuilderExtensions.AddTUnit(builder.Logging, TestContext.Current!); // Or via IServiceCollection -services.AddTUnitLogging(TestContext.Current!); +TUnit.Logging.Microsoft.ServiceCollectionExtensions.AddTUnitLogging(services, TestContext.Current!); ``` All log output is routed through TUnit's console interceptor and sink pipeline, so logs appear in test output, IDE test explorers, and the console (when using `--output Detailed`). @@ -621,28 +622,38 @@ If a resource is shared (database, queue, cache), each test must use its own iso ::: ```csharp -// ❌ BAD: All tests share the same table - will cause flaky failures -protected override void ConfigureTestConfiguration(IConfigurationBuilder config) +public class SharedTableTests : TestsBase { - config.AddInMemoryCollection(new Dictionary + // ❌ BAD: All tests share the same table - will cause flaky failures + protected override void ConfigureTestConfiguration(IConfigurationBuilder config) { - { "Database:TableName", "todos" } // Shared = flaky! - }); + config.AddInMemoryCollection(new Dictionary + { + { "Database:TableName", "todos" } // Shared = flaky! + }); + } } -// ✅ GOOD: Each test gets its own table -protected override async Task SetupAsync() +public class IsolatedTableTests : TestsBase { - TableName = GetIsolatedName("todos"); // "Test_42_todos" - await CreateTableAsync(TableName); -} + private string TableName { get; set; } = null!; -protected override void ConfigureTestConfiguration(IConfigurationBuilder config) -{ - config.AddInMemoryCollection(new Dictionary + // ✅ GOOD: Each test gets its own table + protected override async Task SetupAsync() { - { "Database:TableName", TableName } // Isolated = reliable! - }); + TableName = GetIsolatedName("todos"); // "Test_42_todos" + await CreateTableAsync(TableName); + } + + protected override void ConfigureTestConfiguration(IConfigurationBuilder config) + { + config.AddInMemoryCollection(new Dictionary + { + { "Database:TableName", TableName } // Isolated = reliable! + }); + } + + private static Task CreateTableAsync(string tableName) => Task.CompletedTask; } ``` @@ -691,11 +702,19 @@ public async Task Cleanup() ### 4. Inject Containers at Factory Level ```csharp +public sealed class PostgresContainerFixture : IAsyncInitializer, IAsyncDisposable +{ + public PostgreSqlContainer Container { get; } = new PostgreSqlBuilder("postgres:18").Build(); + + public Task InitializeAsync() => Container.StartAsync(); + public ValueTask DisposeAsync() => Container.DisposeAsync(); +} + public class WebApplicationFactory : TestWebApplicationFactory { // Shared across all tests - [ClassDataSource(Shared = SharedType.PerTestSession)] - public PostgresContainer Postgres { get; init; } = null!; + [ClassDataSource(Shared = SharedType.PerTestSession)] + public PostgresContainerFixture Postgres { get; init; } = null!; [ClassDataSource(Shared = SharedType.PerTestSession)] public RedisContainer Redis { get; init; } = null!; @@ -708,7 +727,7 @@ public class WebApplicationFactory : TestWebApplicationFactory // Container wrapper public class InMemoryPostgres : IAsyncInitializer, IAsyncDisposable { - public PostgreSqlContainer Container { get; } = new PostgreSqlBuilder().Build(); + public PostgreSqlContainer Container { get; } = new PostgreSqlBuilder("postgres:18").Build(); public async Task InitializeAsync() => await Container.StartAsync(); public async ValueTask DisposeAsync() => await Container.DisposeAsync(); } @@ -843,25 +862,32 @@ The key benefits: 3. You're not accidentally reading from a different source (e.g., `appsettings.json`) ```csharp -// Factory sets default -protected override void ConfigureWebHost(IWebHostBuilder builder) +public class DefaultConfigurationFactory : TestWebApplicationFactory { - builder.ConfigureAppConfiguration((_, config) => + // Factory sets default + protected override void ConfigureWebHost(IWebHostBuilder builder) { - config.AddInMemoryCollection(new Dictionary + builder.ConfigureAppConfiguration((_, config) => { - { "Database:ConnectionString", "factory-default" } + config.AddInMemoryCollection(new Dictionary + { + { "Database:ConnectionString", "factory-default" } + }); }); - }); + } } -// Test overrides - this WILL work because it runs after -protected override void ConfigureTestConfiguration(IConfigurationBuilder config) +public class OverrideConfigurationTests + : WebApplicationTest { - config.AddInMemoryCollection(new Dictionary + // Test overrides - this WILL work because it runs after + protected override void ConfigureTestConfiguration(IConfigurationBuilder config) { - { "Database:ConnectionString", "test-specific-value" } // This wins! - }); + config.AddInMemoryCollection(new Dictionary + { + { "Database:ConnectionString", "test-specific-value" } // This wins! + }); + } } ``` @@ -882,10 +908,11 @@ protected override void ConfigureTestConfiguration(IConfigurationBuilder config) ```csharp // BAD: All parallel tests share the same table -var tableName = "todos"; +var sharedTableName = "todos"; // GOOD: Each test gets its own table -var tableName = GetIsolatedName("todos"); // "Test_42_todos", "Test_43_todos", etc. +var isolatedTableName = GetIsolatedName("todos"); // "Test_42_todos", "Test_43_todos", etc. +TestContext.Current!.Output.WriteLine($"{sharedTableName} -> {isolatedTableName}"); ``` ### Can I have different factory configurations for different test classes? @@ -1027,28 +1054,36 @@ public class MyTest : TestsBase **Problem:** You set configuration values in `ConfigureWebHost` using `ConfigureAppConfiguration`, but your app's `Program.cs` doesn't see them during startup. Your breakpoint in Program.cs hits **before** the `ConfigureAppConfiguration` callback. - ```csharp -// Factory - this approach has a timing issue! -protected override void ConfigureWebHost(IWebHostBuilder builder) +public class DeferredConfigurationFactory : TestWebApplicationFactory { - Console.WriteLine("ConfigureWebHost called"); // This runs first... - - builder.ConfigureAppConfiguration((_, config) => + // Factory - this approach has a timing issue! + protected override void ConfigureWebHost(IWebHostBuilder builder) { - Console.WriteLine("ConfigureAppConfiguration callback"); // ...but THIS runs AFTER Program.cs! - config.AddInMemoryCollection(new Dictionary + Console.WriteLine("ConfigureWebHost called"); // This runs first... + + builder.ConfigureAppConfiguration((_, config) => { - { "SomeKey", "SomeValue" } + Console.WriteLine("ConfigureAppConfiguration callback"); // ...but THIS runs AFTER Program.cs! + config.AddInMemoryCollection(new Dictionary + { + { "SomeKey", "SomeValue" } + }); }); - }); + } } -// Program.cs - this runs BEFORE ConfigureAppConfiguration callback! -var builder = WebApplication.CreateBuilder(args); -if (builder.Configuration["SomeKey"] != "SomeValue") +public static class ApplicationStartup { - throw new InvalidOperationException("SomeKey not found!"); // This throws! + public static void Configure(string[] args) + { + // Program.cs - this runs BEFORE ConfigureAppConfiguration callback! + var builder = Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(args); + if (builder.Configuration["SomeKey"] != "SomeValue") + { + throw new InvalidOperationException("SomeKey not found!"); // This throws! + } + } } ``` diff --git a/docs/docs/examples/complex-test-infrastructure.md b/docs/docs/examples/complex-test-infrastructure.md index 1f1d460355c..05c87fd47a8 100644 --- a/docs/docs/examples/complex-test-infrastructure.md +++ b/docs/docs/examples/complex-test-infrastructure.md @@ -1,4 +1,5 @@ - + + # Complex Test Infrastructure Orchestration @@ -37,7 +38,7 @@ public class InMemoryKafka : IAsyncInitializer, IAsyncDisposable [ClassDataSource(Shared = SharedType.PerTestSession)] public required DockerNetwork DockerNetwork { get; init; } - public KafkaContainer Container => field ??= new KafkaBuilder() + public KafkaContainer Container => field ??= new KafkaBuilder("confluentinc/cp-kafka:8.2.0") .WithNetwork(DockerNetwork.Instance) // Uses the injected network .Build(); @@ -59,7 +60,7 @@ public class KafkaUI : IAsyncInitializer, IAsyncDisposable [ClassDataSource(Shared = SharedType.PerTestSession)] public required InMemoryKafka Kafka { get; init; } - public IContainer Container => field ??= new ContainerBuilder() + public IContainer Container => field ??= new ContainerBuilder("confluentinc/cp-enterprise-control-center:8.2.0") .WithNetwork(DockerNetwork.Instance) .WithImage("provectuslabs/kafka-ui:latest") .WithPortBinding(8080, 8080) @@ -116,6 +117,13 @@ public class WebApplicationFactory : WebApplicationFactory, IAsyncIniti }); } } + +public class InMemoryRedis : IAsyncInitializer, IAsyncDisposable +{ + public RedisContainer Container { get; } = new RedisBuilder("redis:8.2").Build(); + public Task InitializeAsync() => Container.StartAsync(); + public ValueTask DisposeAsync() => Container.DisposeAsync(); +} ``` ## Writing Clean Tests @@ -123,7 +131,7 @@ public class WebApplicationFactory : WebApplicationFactory, IAsyncIniti Your actual test code remains clean and focused: ```csharp -public class Tests : TestsBase +public class Tests { [ClassDataSource(Shared = SharedType.PerTestSession)] public required WebApplicationFactory WebApplicationFactory { get; init; } @@ -181,7 +189,7 @@ public class InMemoryPostgreSqlDatabase : IAsyncInitializer, IAsyncDisposable public required DockerNetwork DockerNetwork { get; init; } - public PostgreSqlContainer Container => field ??= new PostgreSqlBuilder() + public PostgreSqlContainer Container => field ??= new PostgreSqlBuilder("postgres:18") .WithUsername("User") .WithPassword("Password") .WithDatabase("TestDatabase") @@ -212,24 +220,26 @@ See the full pattern with `IModelCacheKeyFactory`, `EnsureCreatedAsync()`, and s ### Without TUnit (Traditional Approach) ```csharp +using Xunit; + public class TestFixture : IAsyncLifetime { private INetwork? _network; private KafkaContainer? _kafka; private IContainer? _kafkaUi; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { // Manual orchestration required _network = new NetworkBuilder().Build(); await _network.CreateAsync(); - _kafka = new KafkaBuilder() + _kafka = new KafkaBuilder("confluentinc/cp-kafka:8.2.0") .WithNetwork(_network) .Build(); await _kafka.StartAsync(); - _kafkaUi = new ContainerBuilder() + _kafkaUi = new ContainerBuilder("provectuslabs/kafka-ui:latest") .WithNetwork(_network) .WithEnvironment("KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS", $"{_kafka.Name}:9093") // Manual wiring @@ -237,7 +247,7 @@ public class TestFixture : IAsyncLifetime await _kafkaUi.StartAsync(); } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { // Manual cleanup in reverse order if (_kafkaUi != null) await _kafkaUi.DisposeAsync(); diff --git a/docs/docs/examples/fscheck.md b/docs/docs/examples/fscheck.md index ba76a472251..6d6ed1e0813 100644 --- a/docs/docs/examples/fscheck.md +++ b/docs/docs/examples/fscheck.md @@ -178,7 +178,7 @@ using FsCheck.Fluent; public class Person { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public int Age { get; set; } } diff --git a/docs/docs/examples/instrumenting-global-test-ids.md b/docs/docs/examples/instrumenting-global-test-ids.md index 58662a9cdc7..7cb1d6e7445 100644 --- a/docs/docs/examples/instrumenting-global-test-ids.md +++ b/docs/docs/examples/instrumenting-global-test-ids.md @@ -1,4 +1,3 @@ - # Instrumenting: Global Test IDs @@ -44,7 +43,6 @@ static class TestContextExtensions Assign unique identifiers to all tests in the assembly by decorating in `AssemblyInfo.cs`: - ```csharp [assembly: AssignTestIdentifiers] ``` @@ -90,3 +88,4 @@ class MyTestClassThatNeedsUniqueTestIds The test identifier for each test is assigned in the order that TUnit discovers the tests. The test identifier is unique for each test and is guaranteed to be assigned before the test starts. For other uses cases, you would need to adjust the implementation of `AssignTestIdentifiersAttribute` to suit your needs. For example, you could choose to use GUIDs instead of integers. We've only used integers to match the Redis database number example. + diff --git a/docs/docs/examples/opentelemetry.md b/docs/docs/examples/opentelemetry.md index 6c9113cd44e..fe569269720 100644 --- a/docs/docs/examples/opentelemetry.md +++ b/docs/docs/examples/opentelemetry.md @@ -1,4 +1,3 @@ - # OpenTelemetry Tracing @@ -240,21 +239,15 @@ Swap the exporter in the setup code above. Each exporter needs its own NuGet pac dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol ``` - ```csharp -.AddOtlpExporter(opts => opts.Endpoint = new Uri("http://localhost:4317")) +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddOtlpExporter(opts => opts.Endpoint = new Uri("http://localhost:4317")) + .Build(); ``` ### Zipkin -```bash -dotnet add package OpenTelemetry.Exporter.Zipkin -``` - - -```csharp -.AddZipkinExporter(opts => opts.Endpoint = new Uri("http://localhost:9411/api/v2/spans")) -``` +The Zipkin exporter is obsolete. Export with OTLP to an OpenTelemetry Collector configured with a Zipkin exporter instead. ### ASP.NET Core Integration Tests @@ -326,11 +319,6 @@ For manual setups, add this processor to your tracer builder: using System.Diagnostics; using OpenTelemetry; -// Usage: -using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddProcessor(new TUnitTagProcessor()) - .Build(); - public sealed class TUnitTagProcessor : BaseProcessor { public override void OnStart(Activity activity) @@ -342,6 +330,11 @@ public sealed class TUnitTagProcessor : BaseProcessor } } } + +// Usage: +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(new TUnitTagProcessor()) + .Build(); ``` Register the correlation processor **before** any synchronous exporter (`SimpleExportProcessor`-based). The built-in `TUnitTestCorrelationProcessor` tags at both `OnStart` and `OnEnd`, and a `SimpleExport`-wrapped exporter that runs first would serialize the activity before the tag is applied. `BatchExportProcessor` (the default for OTLP/Jaeger/Zipkin) defers serialization, so order doesn't matter there. diff --git a/docs/docs/execution/cancellation.md b/docs/docs/execution/cancellation.md index ee8837dfc2c..faf1a9161ed 100644 --- a/docs/docs/execution/cancellation.md +++ b/docs/docs/execution/cancellation.md @@ -1,10 +1,8 @@ - # Cancelling a Test Call `TestContext.Execution.Cancel()` to request cooperative cancellation of the current test without affecting other tests or the test session: - ```csharp [Test] public async Task ProcessMessages(CancellationToken cancellationToken) diff --git a/docs/docs/execution/engine-modes.md b/docs/docs/execution/engine-modes.md index 1bd514da83e..c62adca5b41 100644 --- a/docs/docs/execution/engine-modes.md +++ b/docs/docs/execution/engine-modes.md @@ -1,4 +1,3 @@ - # Engine Modes @@ -56,19 +55,21 @@ This is the recommended approach when you need reflection mode for a specific te **Example: bUnit Test Project** ```csharp +using Bunit; + // Add this to enable reflection mode for your bUnit tests [assembly: ReflectionMode] namespace MyApp.Tests; -public class CounterComponentTests : TestContext +public class CounterComponentTests : BunitContext { [Test] - public void CounterStartsAtZero() + public async Task CounterStartsAtZero() { // Test Razor components that are source-generated at compile time - var cut = RenderComponent(); - cut.Find("p").TextContent.ShouldBe("Current count: 0"); + var cut = Render(); + await Assert.That(cut.Find("p").TextContent).IsEqualTo("Current count: 0"); } } ``` @@ -115,7 +116,7 @@ Add this MSBuild property to your test project file (`.csproj`): - + @@ -123,18 +124,20 @@ Add this MSBuild property to your test project file (`.csproj`): Then in your code: ```csharp +using Bunit; + // Enable reflection mode for Razor component testing [assembly: ReflectionMode] namespace MyApp.Tests; -public class CounterComponentTests : TestContext +public class CounterComponentTests : BunitContext { [Test] - public void CounterStartsAtZero() + public async Task CounterStartsAtZero() { - var cut = RenderComponent(); - cut.Find("p").TextContent.ShouldBe("Current count: 0"); + var cut = Render(); + await Assert.That(cut.Find("p").TextContent).IsEqualTo("Current count: 0"); } } ``` diff --git a/docs/docs/execution/parallelism.md b/docs/docs/execution/parallelism.md index fe68d80b160..f52a7f0f17e 100644 --- a/docs/docs/execution/parallelism.md +++ b/docs/docs/execution/parallelism.md @@ -2,7 +2,6 @@ sidebar_position: 10 --- - # Controlling Parallelism @@ -106,7 +105,7 @@ public class OrderRepositoryTests public async Task Create_Order() { var order = await OrderRepository.CreateAsync("item-1"); - await Assert.That(order.Id).IsNotNull(); + await Assert.That(order.Id).IsNotEmptyGuid(); } } @@ -163,9 +162,13 @@ With a limit of `2`, at most two of these 20 test invocations execute at the sam ### Assembly-Level Limiter - ```csharp -[assembly: ParallelLimiter] +[assembly: ParallelLimiter] + +public record MyAssemblyParallelLimit : IParallelLimit +{ + public int Limit => 2; +} ``` More specific attributes override less specific ones. Precedence: Method > Class > Assembly. diff --git a/docs/docs/execution/parameters.md b/docs/docs/execution/parameters.md index 539240d3d84..722d2429bcd 100644 --- a/docs/docs/execution/parameters.md +++ b/docs/docs/execution/parameters.md @@ -1,4 +1,3 @@ - # Test Parameters @@ -51,7 +50,6 @@ public class MyTests ### Environment-specific configuration - ```csharp [Before(Test)] public void SetupEnvironment() @@ -65,14 +63,13 @@ public void SetupEnvironment() ### Conditional test logic - ```csharp [Test] public async Task IntegrationTest() { if (!TestContext.Parameters.ContainsKey("run-integration")) { - Assert.Skip("Integration tests require --test-parameter run-integration=true"); + Skip.Test("Integration tests require --test-parameter run-integration=true"); } // Run the integration test... @@ -81,17 +78,17 @@ public async Task IntegrationTest() ### Passing secrets or connection strings - ```csharp [Test] public async Task DatabaseTest() { if (!TestContext.Parameters.TryGetValue("connection-string", out var connectionStrings)) { - Assert.Skip("Requires --test-parameter connection-string=..."); + Skip.Test("Requires --test-parameter connection-string=..."); } - using var connection = new SqlConnection(connectionStrings.First()); + await using var connection = new NpgsqlConnection( + connectionStrings.FirstOrDefault() ?? throw new InvalidOperationException("Missing connection string")); // ... } ``` diff --git a/docs/docs/execution/timeouts.md b/docs/docs/execution/timeouts.md index abb0f7e579d..096ac7a031e 100644 --- a/docs/docs/execution/timeouts.md +++ b/docs/docs/execution/timeouts.md @@ -2,7 +2,6 @@ sidebar_position: 5 --- - # Timeouts @@ -42,7 +41,6 @@ If the HTTP call takes longer than 30 seconds, `cancellationToken` is cancelled, 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)] diff --git a/docs/docs/extending/argument-formatters.md b/docs/docs/extending/argument-formatters.md index b04eb440c83..17364906da7 100644 --- a/docs/docs/extending/argument-formatters.md +++ b/docs/docs/extending/argument-formatters.md @@ -1,4 +1,3 @@ - # Argument Formatters @@ -15,8 +14,10 @@ For example: [ArgumentDisplayFormatter] public async Task Test(SomeClass someClass) { - await Assert.That(TestContext.Current!.GetDisplayName()).IsEqualTo("A super important test!"); + await Assert.That(TestContext.Current!.Metadata.DisplayName).IsEqualTo("A super important test!"); } + + public static IEnumerable SomeMethod() => [new SomeClass()]; ``` ```csharp @@ -29,7 +30,11 @@ public class MyFormatter : ArgumentDisplayFormatter public override string FormatValue(object? value) { - var someClass = (SomeClass)value; + if (value is not SomeClass someClass) + { + throw new ArgumentException("Value must be a SomeClass instance.", nameof(value)); + } + return $"One: {someClass.One} | Two: {someClass.Two}"; } } @@ -39,3 +44,4 @@ public class MyFormatter : ArgumentDisplayFormatter You can apply multiple `[ArgumentDisplayFormatter]` attributes if you have different types to format. The first formatter whose `CanHandle` returns true will be used. ::: + diff --git a/docs/docs/extending/data-source-generators.md b/docs/docs/extending/data-source-generators.md index 06cb4b1eb62..fcf56291309 100644 --- a/docs/docs/extending/data-source-generators.md +++ b/docs/docs/extending/data-source-generators.md @@ -1,4 +1,3 @@ - # Data Source Generators @@ -36,7 +35,7 @@ public class MyTestClass(SomeClass1 someClass1, SomeClass2 someClass2, SomeClass [AutoFixtureGenerator] public async Task Test(int value, string value2, bool value3) { - // ... + _ = (someClass1, someClass2, someClass3, value, value2, value3); } } @@ -74,15 +73,11 @@ public class DatabaseDataGeneratorAttribute : AsyncDataSourceGeneratorAttribu protected override async IAsyncEnumerable>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata) { - await using var connection = new SqlConnection(_connectionString); + await using var connection = new NpgsqlConnection(_connectionString); await connection.OpenAsync(); - - var entities = await connection.QueryAsync("SELECT * FROM " + typeof(T).Name); - - foreach (var entity in entities) - { - yield return () => Task.FromResult(entity); - } + + var fixture = new Fixture(); + yield return () => Task.FromResult(fixture.Create()); } } @@ -146,7 +141,7 @@ public class RepositoryTests(DatabaseContext context) [Test] public async Task TestRepository() { - // context is populated by AutoFixture + _ = context; } } ``` @@ -166,18 +161,21 @@ After each `yield`, the execution is passed back to TUnit, and TUnit will set a The `TestBuilderContext` object exposes `Events` - And you can register a delegate to be invoked on them at the point in the test lifecycle that you wish. ```csharp -public override IEnumerable> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) +public sealed class ContextAwareDataGeneratorAttribute : DataSourceGeneratorAttribute { - dataGeneratorMetadata.TestBuilderContext.Current; // <-- Initial Context for first test - - yield return () => 1; - - dataGeneratorMetadata.TestBuilderContext.Current; // <-- This is now a different context object, as we yielded - dataGeneratorMetadata.TestBuilderContext.Current; // <-- This is still the same as above because it'll only change on a yield - - yield return () => 2; - - dataGeneratorMetadata.TestBuilderContext.Current; // <-- A new object again + protected override IEnumerable> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) + { + _ = dataGeneratorMetadata.TestBuilderContext.Current; // Initial context for first test + + yield return () => 1; + + _ = dataGeneratorMetadata.TestBuilderContext.Current; // A different context after yielding + _ = dataGeneratorMetadata.TestBuilderContext.Current; // Still the same until the next yield + + yield return () => 2; + + _ = dataGeneratorMetadata.TestBuilderContext.Current; // A new context again + } } ``` diff --git a/docs/docs/extending/exception-handling.md b/docs/docs/extending/exception-handling.md index 7e2ac14f89b..5173ba75362 100644 --- a/docs/docs/extending/exception-handling.md +++ b/docs/docs/extending/exception-handling.md @@ -1,4 +1,3 @@ - # Exception Handling @@ -8,7 +7,6 @@ When a test fails, TUnit throws an exception. Most of the time you don't need to If a test can't run because of some runtime condition, throw `SkipTestException`. The test will be reported as skipped rather than failed. - ```csharp [Test] public async Task RequiresExternalService() @@ -26,7 +24,6 @@ public async Task RequiresExternalService() If a test can't determine a pass/fail result, throw `InconclusiveTestException`. - ```csharp [Test] public async Task CheckFeatureFlag() @@ -46,7 +43,6 @@ public async Task CheckFeatureFlag() In an `[After(Test)]` hook, you can check whether the test failed via `TestContext`: - ```csharp [After(Test)] public async Task TakeScreenshotOnFailure(TestContext context) diff --git a/docs/docs/extending/extension-points.md b/docs/docs/extending/extension-points.md index d21d68f1385..c93c66e3c7d 100644 --- a/docs/docs/extending/extension-points.md +++ b/docs/docs/extending/extension-points.md @@ -1,4 +1,3 @@ - # Extension Points @@ -37,7 +36,7 @@ public class TimingTestExecutor : ITestExecutor finally { stopwatch.Stop(); - context.WriteLine($"Test execution took: {stopwatch.ElapsedMilliseconds}ms"); + context.Output.WriteLine($"Test execution took: {stopwatch.ElapsedMilliseconds}ms"); // You could also send this to telemetry TelemetryClient.TrackMetric("TestDuration", stopwatch.ElapsedMilliseconds); @@ -52,13 +51,15 @@ To use your custom test executor, apply the `TestExecutorAttribute` at the assem ```csharp // Assembly-level (applies to all tests in the assembly) -[assembly: TestExecutor] +[assembly: TestExecutor] -// Or use the non-generic version -[assembly: TestExecutor(typeof(TimingTestExecutor))] +public sealed class RegistrationTimingTestExecutor : ITestExecutor +{ + public ValueTask ExecuteTest(TestContext context, Func action) => action(); +} // Class-level (applies to all tests in the class) -[TestExecutor] +[TestExecutor] public class MyTestClass { [Test] @@ -68,12 +69,15 @@ public class MyTestClass } } -// Method-level (applies to specific test) -[Test] -[TestExecutor] -public async Task MyTest() +public class MethodLevelExecutorTests { - // Test logic here + // Method-level (applies to specific test) + [Test] + [TestExecutor] + public async Task MyTest() + { + // Test logic here + } } ``` @@ -232,10 +236,15 @@ You can also apply it at the class or assembly level to affect all hooks in that ```csharp // Assembly-level (applies to all hooks in the assembly) -[assembly: HookExecutor] +[assembly: HookExecutor] + +public sealed class AssemblyLoggingHookExecutor : GenericAbstractExecutor +{ + protected override ValueTask ExecuteAsync(Func action) => action(); +} // Class-level (applies to all hooks in the class) -[HookExecutor] +[HookExecutor] public class MyTestClass { [Before(Test)] @@ -258,9 +267,8 @@ public class DispatchAttribute : Attribute, ITestRegisteredEventReceiver public ValueTask OnTestRegistered(TestRegisteredContext context) { - var executor = new MyCustomExecutor(); - context.SetTestExecutor(executor); - context.SetHookExecutor(executor); + context.SetTestExecutor(new TimingTestExecutor()); + context.SetHookExecutor(new LoggingHookExecutor()); return default; } } @@ -355,7 +363,7 @@ public class TestReporterAttribute : Attribute, ITestStartEventReceiver, ITestEn public async ValueTask OnTestStart(TestContext context) { await ReportingService.ReportTestStarted( - context.GetDisplayName(), + context.Metadata.DisplayName, context.Metadata.TestDetails.ClassType.FullName, context.Metadata.TestDetails.TestMethodArguments ); @@ -364,7 +372,7 @@ public class TestReporterAttribute : Attribute, ITestStartEventReceiver, ITestEn public async ValueTask OnTestEnd(TestContext context) { await ReportingService.ReportTestCompleted( - context.GetDisplayName(), + context.Metadata.DisplayName, context.Execution.Result?.State, context.Execution.Result?.Duration, context.Execution.Result?.Exception?.Message @@ -378,6 +386,9 @@ public class TestReporterAttribute : Attribute, ITestStartEventReceiver, ITestEn Event receivers are registered by implementing the interfaces in an attribute class, then applying that attribute at the assembly, class, or method level: ```csharp +// Apply at assembly level +[assembly: CustomEventReceiver] + // Create an attribute that implements the event receiver interfaces [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] public class CustomEventReceiverAttribute : Attribute, ITestStartEventReceiver, ITestEndEventReceiver @@ -386,20 +397,17 @@ public class CustomEventReceiverAttribute : Attribute, ITestStartEventReceiver, public ValueTask OnTestStart(TestContext context) { - Console.WriteLine($"Test starting: {context.GetDisplayName()}"); + Console.WriteLine($"Test starting: {context.Metadata.DisplayName}"); return default; } public ValueTask OnTestEnd(TestContext context) { - Console.WriteLine($"Test ended: {context.GetDisplayName()} - {context.Execution.Result?.State}"); + Console.WriteLine($"Test ended: {context.Metadata.DisplayName} - {context.Execution.Result?.State}"); return default; } } -// Apply at assembly level -[assembly: CustomEventReceiver] - // Or at class level [CustomEventReceiver] public class MyTestClass @@ -408,10 +416,13 @@ public class MyTestClass public async Task MyTest() { } } -// Or at method level -[Test] -[CustomEventReceiver] -public async Task MyTest() { } +public class MethodEventReceiverTests +{ + // Or at method level + [Test] + [CustomEventReceiver] + public Task MyTest() => Task.CompletedTask; +} ``` ## Parallel Execution Control @@ -516,7 +527,7 @@ Example: ```csharp public class DatabaseTests : IAsyncInitializer { - private DatabaseConnection _connection; + private DatabaseConnection _connection = null!; public async Task InitializeAsync() { @@ -565,7 +576,7 @@ public class TestCaseFixture : IAsyncDiscoveryInitializer, IAsyncDisposable public async Task InitializeAsync() { // This runs during DISCOVERY, not just execution - _testCases = await LoadTestCasesFromDatabaseAsync(); + _testCases = [.. await LoadTestCasesFromDatabaseAsync()]; } public IEnumerable GetTestCases() => _testCases; @@ -639,7 +650,7 @@ public class TransactionalTestExecutor : ITestExecutor public async ValueTask ExecuteTest(TestContext context, Func action) { // Get the database connection from DI - var dbContext = context.GetService(); + var dbContext = new ApplicationDbContext(); using var transaction = await dbContext.Database.BeginTransactionAsync(); diff --git a/docs/docs/extending/libraries.md b/docs/docs/extending/libraries.md index 3ca9d24d866..81e44a3858b 100644 --- a/docs/docs/extending/libraries.md +++ b/docs/docs/extending/libraries.md @@ -1,4 +1,3 @@ - # Libraries @@ -85,7 +84,7 @@ public class OrderTests : DatabaseTestBase { var order = await OrderService.CreateAsync("item-1"); - await Assert.That(order.Id).IsNotNull(); + await Assert.That(order.Id).IsNotEqualTo(Guid.Empty); } } ``` diff --git a/docs/docs/extending/logging.md b/docs/docs/extending/logging.md index c71136377a8..d1222a268e5 100644 --- a/docs/docs/extending/logging.md +++ b/docs/docs/extending/logging.md @@ -1,4 +1,3 @@ - # Logging @@ -9,6 +8,8 @@ TUnit provides a flexible logging system that captures all test output and route By default, TUnit intercepts any logs to `Console.WriteLine()` and correlates them to the test that triggered the log using the current async context. ```csharp +using TUnit.Core.Logging; + [Test] public async Task MyTest() { @@ -21,6 +22,8 @@ public async Task MyTest() For more control, use `TestContext.Current.GetDefaultLogger()` to get a logger instance: ```csharp +using TUnit.Core.Logging; + [Test] public async Task MyTest() { @@ -229,6 +232,8 @@ Implement the `ILogSink` interface to create a custom sink: ```csharp using TUnit.Core; using TUnit.Core.Logging; +using Serilog; +using LogLevel = TUnit.Core.Logging.LogLevel; public class FileLogSink : ILogSink, IAsyncDisposable { @@ -285,10 +290,10 @@ public class TestSetup public static void SetupLogging() { // Register by instance (for sinks needing configuration) - TUnitLoggerFactory.AddSink(new FileLogSink("test-output.log")); + TUnit.Core.Logging.TUnitLoggerFactory.AddSink(new FileLogSink("test-output.log")); // Or register by type (for simple sinks) - TUnitLoggerFactory.AddSink(); + TUnit.Core.Logging.TUnitLoggerFactory.AddSink(); } } ``` @@ -300,6 +305,8 @@ Sinks that implement `IDisposable` or `IAsyncDisposable` are automatically dispo The `context` parameter provides information about where the log originated: ```csharp +using LogLevel = TUnit.Core.Logging.LogLevel; + public void Log(LogLevel level, string message, Exception? exception, Context? context) { switch (context) @@ -332,13 +339,17 @@ public void Log(LogLevel level, string message, Exception? exception, Context? c Here's an example sink that sends logs to Seq: ```csharp +using TUnit.Core.Logging; +using Serilog; +using LogLevel = TUnit.Core.Logging.LogLevel; + public class SeqLogSink : ILogSink, IDisposable { private readonly Serilog.ILogger _logger; public SeqLogSink(string seqUrl) { - _logger = new LoggerConfiguration() + _logger = new Serilog.LoggerConfiguration() .WriteTo.Seq(seqUrl) .CreateLogger(); } @@ -408,7 +419,7 @@ using TUnit.Logging.Microsoft; var host = Host.CreateDefaultBuilder() .ConfigureLogging(logging => { - logging.AddTUnit(TestContext.Current!); + TUnit.Logging.Microsoft.LoggingBuilderExtensions.AddTUnit(logging, TestContext.Current!); }) .Build(); ``` @@ -416,7 +427,7 @@ var host = Host.CreateDefaultBuilder() Or via `IServiceCollection`: ```csharp -services.AddTUnitLogging(TestContext.Current!); +TUnit.Logging.Microsoft.ServiceCollectionExtensions.AddTUnitLogging(services, TestContext.Current!); ``` All `ILogger` output is routed through TUnit's console interceptor and sink pipeline, appearing in test output, IDE test explorers, and the console. @@ -442,6 +453,9 @@ Available levels (from least to most severe): You can also create custom loggers by inheriting from `DefaultLogger`: ```csharp +using TUnit.Core.Logging; +using LogLevel = TUnit.Core.Logging.LogLevel; + public class TestHeaderLogger : DefaultLogger { private bool _hasOutputHeader; diff --git a/docs/docs/getting-started/writing-your-first-test.md b/docs/docs/getting-started/writing-your-first-test.md index 528e6364c1a..408662b77fb 100644 --- a/docs/docs/getting-started/writing-your-first-test.md +++ b/docs/docs/getting-started/writing-your-first-test.md @@ -1,4 +1,3 @@ - # Writing your first test @@ -81,7 +80,6 @@ Tests will pass if they execute successfully without any exceptions. Test methods can be either synchronous or asynchronous: - ```csharp [Test] public void SynchronousTest() // ✅ Valid - synchronous test diff --git a/docs/docs/guides/distributed-tracing.md b/docs/docs/guides/distributed-tracing.md index 40b949ce2e4..8be3e56b3fd 100644 --- a/docs/docs/guides/distributed-tracing.md +++ b/docs/docs/guides/distributed-tracing.md @@ -2,7 +2,6 @@ sidebar_position: 20 --- - # Distributed Tracing @@ -55,14 +54,15 @@ Install [`TUnit.OpenTelemetry`](/docs/examples/opentelemetry#option-a-zero-confi Point the OTLP exporter at Seq's ingestion endpoint: - ```csharp -.AddOtlpExporter(opts => -{ - opts.Endpoint = new Uri("http://localhost:5341/ingest/otlp/v1/traces"); - opts.Protocol = OtlpExportProtocol.HttpProtobuf; - opts.Headers = "X-Seq-ApiKey=your-key"; -}) +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddOtlpExporter(opts => + { + opts.Endpoint = new Uri("http://localhost:5341/ingest/otlp/v1/traces"); + opts.Protocol = OtlpExportProtocol.HttpProtobuf; + opts.Headers = "X-Seq-ApiKey=your-key"; + }) + .Build(); ``` Useful Seq queries: @@ -76,9 +76,10 @@ test.case.result.status = 'fail' -- only failures ### Jaeger or Tempo - ```csharp -.AddOtlpExporter(opts => opts.Endpoint = new Uri("http://localhost:4317")) +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddOtlpExporter(opts => opts.Endpoint = new Uri("http://localhost:4317")) + .Build(); ``` Jaeger groups by trace ID, so each test appears as a separate trace. Use the tag search box (`tunit.session.id=""`) to find all traces from one run. @@ -113,7 +114,6 @@ TUnit handles this automatically: a module initializer in `TUnit.Core` replaces For the SUT side, if it shares the test process (e.g. `TestWebApplicationFactory`), alignment flows automatically. For out-of-process SUTs that don't reference `TUnit.Core`, align the propagator yourself on startup — either match `DistributedContextPropagator.Current` or, if you use the OpenTelemetry SDK: - ```csharp using OpenTelemetry; using OpenTelemetry.Context.Propagation; @@ -148,7 +148,6 @@ Use [`TestWebApplicationFactory`](/docs/examples/aspnet) or wrap with `Traced Opt out per-test when the SUT already instruments its own outbound HTTP (for example via the OpenTelemetry HttpClient instrumentation) by setting `WebApplicationTestOptions.AutoPropagateHttpClientFactory = false`: - ```csharp protected override void ConfigureTestOptions(WebApplicationTestOptions options) { @@ -172,22 +171,19 @@ Install [`TUnit.OpenTelemetry`](/docs/examples/opentelemetry#option-a-zero-confi Read the endpoint from `AutoReceiver.Endpoint` and plumb it into the SUT: - ```csharp using TUnit.OpenTelemetry; var endpoint = AutoReceiver.Endpoint; // e.g. "http://127.0.0.1:41234" +using var process = new Process { StartInfo = new ProcessStartInfo("dotnet") }; process.StartInfo.EnvironmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] = endpoint; process.StartInfo.EnvironmentVariables["OTEL_EXPORTER_OTLP_PROTOCOL"] = "http/protobuf"; ``` For the receiver to associate incoming spans with the right test, register the SUT's trace ID before it runs: - ```csharp -using TUnit.Engine.Reporters.Html; - -ActivityCollector.Current?.RegisterExternalTrace(Activity.Current!.TraceId.ToString()); +TestContext.Current!.RegisterTrace(Activity.Current!.TraceId); ``` Spans arriving on a trace ID that wasn't registered are dropped (protects the report from unrelated traffic on shared runners). Each registered trace is capped at 100 external spans. diff --git a/docs/docs/guides/html-report.md b/docs/docs/guides/html-report.md index 251b21bd3c9..16c02002948 100644 --- a/docs/docs/guides/html-report.md +++ b/docs/docs/guides/html-report.md @@ -2,7 +2,6 @@ sidebar_position: 10 --- - # HTML Test Report @@ -22,7 +21,7 @@ The OS and runtime version are included automatically so that matrix builds (mul Open it in any modern browser. The report is fully self-contained (single HTML file) and works offline. -A machine-readable JSON sidecar (`{AssemblyName}-{os}-{tfm}.tunit-report.json`) is written alongside the HTML report. It powers [report aggregation](/docs/guides/report-aggregation) — merging reports from multiple test projects into one — and can be disabled with `TUNIT_DISABLE_JSON_REPORT=true`. +A machine-readable JSON sidecar (`{AssemblyName}-{os}-{tfm}.tunit-report.json`) is written alongside the HTML report. It powers [report aggregation](/docs/guides/report-aggregation) — merging reports from multiple test projects into one — and can be disabled with `TUNIT_DISABLE_JSON_REPORT=true` or `context.Settings.Reporting.JsonReportEnabled = false`. Running many test projects and want **one combined report instead of one per project**? See [Aggregated Reports](/docs/guides/report-aggregation). @@ -54,6 +53,8 @@ export TUNIT_DISABLE_HTML_REPORTER=true Accepts: `true`, `1`, `yes` (case-insensitive). +For version-controlled project configuration, set `context.Settings.Reporting.HtmlReportEnabled = false` in a `[Before(HookType.TestDiscovery)]` hook instead. + ### Deprecated: `--report-html` Flag The `--report-html` flag is deprecated since the report is now generated by default. Using it will show a deprecation warning but will not cause an error. @@ -136,6 +137,8 @@ This is useful if you: The report file and the `GITHUB_STEP_SUMMARY` are still generated. +This can also be configured in code with `context.Settings.Reporting.ArtifactUploadEnabled = false`. + ### Viewing the Report After the workflow run completes: @@ -158,7 +161,6 @@ TUnit's test body runs under a per-test `System.Diagnostics.Activity`. Because ` For example, an integration test using `WebApplicationFactory`: - ```csharp [Test] public async Task GetUsers_ReturnsOk() @@ -180,16 +182,15 @@ The HTML report groups each test under its class. Backends like Seq, Jaeger, and If your test communicates with an external service that runs in a **separate process** (and therefore has a different trace context), you can manually link its trace to the test: - ```csharp [Test] public async Task ProcessOrder_SendsNotification() { - // Start some external work that creates its own trace - var externalActivity = MyExternalService.StartProcessing(orderId); + // Obtain this from the external service's trace response or diagnostics. + var externalTraceId = ActivityTraceId.CreateRandom(); // Link that trace to this test so it appears in the HTML report - TestContext.Current!.RegisterTrace(externalActivity.Context.TraceId); + TestContext.Current!.RegisterTrace(externalTraceId); // ... wait for processing, assert results } @@ -201,7 +202,6 @@ Linked traces appear as a separate **"Linked Trace"** section below the test's m You can access the current test's `Activity` to parent external work explicitly: - ```csharp [Test] public async Task MyTest() @@ -238,7 +238,7 @@ The collector uses **smart sampling**: spans from known test traces are fully re ### Report Not Generated -- Check that `TUNIT_DISABLE_HTML_REPORTER` is not set in your environment +- Check that `TUNIT_DISABLE_HTML_REPORTER` is not set and `context.Settings.Reporting.HtmlReportEnabled` is not `false` - Verify that the `TestResults/` directory is writable - Check the console output for any warning messages about report generation failures diff --git a/docs/docs/guides/performance.md b/docs/docs/guides/performance.md index e1c7f9b8f15..fcdd1cbf253 100644 --- a/docs/docs/guides/performance.md +++ b/docs/docs/guides/performance.md @@ -1,4 +1,3 @@ - # Performance Best Practices @@ -36,7 +35,7 @@ Benefits: ```csharp // ❌ Bad: Heavy computation during discovery -public static IEnumerable GetTestUsers() +public static IEnumerable GetTestUsersWithDatabaseQuery() { // This runs during test discovery! var users = DatabaseQuery.GetAllUsers(); @@ -44,7 +43,7 @@ public static IEnumerable GetTestUsers() } // ✅ Good: Lightweight data generation -public static IEnumerable GetTestUsers() +public static IEnumerable GetLightweightTestUsers() { yield return new User { Id = 1, Name = "Test User 1" }; yield return new User { Id = 2, Name = "Test User 2" }; @@ -84,7 +83,7 @@ Remember that each `[Arguments(...)]` attribute produces exactly **one** test ca // ❌ Bad: Combinatorial explosion via [Matrix] [Test] [MatrixDataSource] -public void Process( +public void ProcessAllCombinations( [Matrix(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)] int count, [Matrix("a", "b", "c", "d", "e")] string label, [Matrix(true, false)] bool flag) @@ -97,7 +96,7 @@ public void Process( [Arguments(1, "a", true)] [Arguments(5, "c", false)] [Arguments(10, "e", true)] -public void Process(int count, string label, bool flag) +public void ProcessTargetedCases(int count, string label, bool flag) { // Only 3 specific test cases — each [Arguments] attribute is one test } @@ -189,8 +188,8 @@ public class ExpensiveTests [Before(Test)] public async Task SetupEachTest() { - await StartDatabaseContainer(); - await MigrateDatabase(); + await DatabaseInfrastructure.StartDatabaseContainer(); + await DatabaseInfrastructure.MigrateDatabase(); } } @@ -202,8 +201,8 @@ public class EfficientTests [Before(Class)] public static async Task SetupOnce() { - _container = await StartDatabaseContainer(); - await MigrateDatabase(); + _container = await DatabaseInfrastructure.StartDatabaseContainer(); + await DatabaseInfrastructure.MigrateDatabase(); } [After(Class)] @@ -215,6 +214,19 @@ public class EfficientTests } } } + +public sealed class DatabaseContainer : IAsyncDisposable +{ + public ValueTask DisposeAsync() => ValueTask.CompletedTask; +} + +public static class DatabaseInfrastructure +{ + public static Task StartDatabaseContainer() + => Task.FromResult(new DatabaseContainer()); + + public static Task MigrateDatabase() => Task.CompletedTask; +} ``` #### Use Lazy Initialization @@ -232,6 +244,11 @@ public class PerformantTests await resource.DoSomethingAsync(); } } + +public sealed class ExpensiveResource +{ + public Task DoSomethingAsync() => Task.CompletedTask; +} ``` ### Optimize Assertions diff --git a/docs/docs/guides/philosophy.md b/docs/docs/guides/philosophy.md index d5a766a8464..df5ed673958 100644 --- a/docs/docs/guides/philosophy.md +++ b/docs/docs/guides/philosophy.md @@ -1,4 +1,3 @@ - # Philosophy @@ -10,7 +9,6 @@ Most frameworks make you opt into parallelism. TUnit flips that — tests run in This also nudges you toward better test design. If your tests can't run in parallel, they're probably sharing state they shouldn't be. When they genuinely do need exclusive access to something (a shared file, a database, a hardware device), you opt out explicitly: - ```csharp [Test, NotInParallel] public async Task ModifiesSharedConfigFile() { ... } @@ -26,7 +24,6 @@ If you need shared state, use `static`. That makes the sharing visible to anyone All assertions return `Task` and must be awaited. This is probably TUnit's most controversial decision. - ```csharp await Assert.That(result).IsEqualTo(expected); ``` @@ -49,7 +46,6 @@ The trade-off is that some older tools only work with VSTest — Coverlet being TUnit's assertions are extension methods on specific types, not generic methods that accept anything. Intellisense only shows assertions that make sense for what you're testing. You can't accidentally check if a string is negative, because that method doesn't exist on strings. - ```csharp await Assert.That(user.Email) .IsNotNull() diff --git a/docs/docs/guides/report-aggregation.md b/docs/docs/guides/report-aggregation.md index 6cc1e5881b6..f399d78b215 100644 --- a/docs/docs/guides/report-aggregation.md +++ b/docs/docs/guides/report-aggregation.md @@ -12,7 +12,7 @@ Report aggregation merges all of that into **one combined HTML report and one Gi ## How It Works -1. Alongside every HTML report, TUnit writes a machine-readable sidecar: `{AssemblyName}-{os}-{tfm}.tunit-report.json`. This is on by default (disable with `TUNIT_DISABLE_JSON_REPORT=true`). +1. Alongside every HTML report, TUnit writes a machine-readable sidecar: `{AssemblyName}-{os}-{tfm}.tunit-report.json`. This is on by default (disable with `TUNIT_DISABLE_JSON_REPORT=true` or `context.Settings.Reporting.JsonReportEnabled = false`). 2. With aggregation enabled, each test process also copies its sidecar into a directory shared by all sibling processes. 3. As each process finishes, it takes a cross-process lock, reads *all* sidecars present so far, and regenerates the merged HTML report and the summary block. The last process to finish naturally leaves the complete aggregate — no process ever needs to know whether it is the last one. @@ -166,11 +166,11 @@ tunit-report merge --directory [options] | --- | --- | | `TUNIT_AGGREGATE_REPORTS` | Unset (default) — cooperative merge wherever a shared directory is resolvable (GitHub Actions, or explicit `TUNIT_AGGREGATE_DIR`); silently off otherwise. `defer` — persist sidecars + merged HTML only; no summary blocks (multi-step scenarios). `off` (also `false`/`0`/`no`/`disabled`/`none`) — no aggregation. | | `TUNIT_AGGREGATE_DIR` | Shared directory for sidecars and the merged report. Required outside GitHub Actions; optional override on GitHub Actions. | -| `TUNIT_DISABLE_JSON_REPORT` | Disables the JSON sidecar written next to the HTML report. Note: sidecars are what aggregation and `tunit-report` consume. | +| `TUNIT_DISABLE_JSON_REPORT` | Disables the JSON sidecar written next to the HTML report. Programmatic equivalent: `context.Settings.Reporting.JsonReportEnabled = false`. Note: sidecars are what aggregation and `tunit-report` consume. | ## Notes & Limitations -- Aggregation is driven by the HTML reporter's data pipeline — if you set `TUNIT_DISABLE_HTML_REPORTER`, no sidecars are produced and there is nothing to merge. +- Aggregation is driven by the HTML reporter's data pipeline — if you set `TUNIT_DISABLE_HTML_REPORTER` or `context.Settings.Reporting.HtmlReportEnabled = false`, no sidecars are produced and there is nothing to merge. - With cooperative mode (the default) across *multiple steps in the same job*, each step appends its own progressively-larger block (earlier steps' blocks can't be rewritten). Use `defer` + the tool for that layout, or `off` to restore per-suite blocks. - Suites are identified per assembly + OS + TFM, so multi-targeted projects appear as separate rows (e.g. `MyTests (.NET 8.0.x)` / `MyTests (.NET 9.0.x)`). - The GitHub step summary is capped at 1 MB by GitHub; the aggregated block replaces N per-suite blocks, so it usually *reduces* summary size. diff --git a/docs/docs/migration/mstest.md b/docs/docs/migration/mstest.md index bf100166999..daa40e99b06 100644 --- a/docs/docs/migration/mstest.md +++ b/docs/docs/migration/mstest.md @@ -1,4 +1,3 @@ - # Migrating from MSTest @@ -262,16 +261,16 @@ Assert.AreEqual(expected, actual); Assert.AreNotEqual(expected, actual); Assert.IsTrue(condition); Assert.IsFalse(condition); -Assert.IsNull(value); -Assert.IsNotNull(value); +Assert.IsNull(optional); +Assert.IsNotNull(obj); // TUnit await Assert.That(actual).IsEqualTo(expected); await Assert.That(actual).IsNotEqualTo(expected); await Assert.That(condition).IsTrue(); await Assert.That(condition).IsFalse(); -await Assert.That(value).IsNull(); -await Assert.That(value).IsNotNull(); +await Assert.That(optional).IsNull(); +await Assert.That(obj).IsNotNull(); ``` #### Reference Assertions @@ -300,18 +299,18 @@ await Assert.That(value).IsNotAssignableTo(); ```csharp // MSTest -CollectionAssert.AreEqual(expected, actual); -CollectionAssert.AreNotEqual(expected, actual); +CollectionAssert.AreEqual(values, numbers); +CollectionAssert.AreNotEqual(values, otherNumbers); CollectionAssert.Contains(collection, item); CollectionAssert.DoesNotContain(collection, item); -CollectionAssert.AllItemsAreNotNull(collection); +CollectionAssert.AllItemsAreNotNull(objects); // TUnit -await Assert.That(actual).IsEquivalentTo(expected); -await Assert.That(actual).IsNotEquivalentTo(expected); +await Assert.That(numbers).IsEquivalentTo(values); +await Assert.That(otherNumbers).IsNotEquivalentTo(values); await Assert.That(collection).Contains(item); await Assert.That(collection).DoesNotContain(item); -await Assert.That(collection).All().Satisfy(item => item.IsNotNull()); +await Assert.That(objects).All().Satisfy(item => item.IsNotNull()); ``` ### String Assertions @@ -321,7 +320,7 @@ await Assert.That(collection).All().Satisfy(item => item.IsNotNull()); StringAssert.Contains(text, substring); StringAssert.StartsWith(text, prefix); StringAssert.EndsWith(text, suffix); -StringAssert.Matches(text, pattern); +StringAssert.Matches(text, new Regex(pattern)); // TUnit await Assert.That(text).Contains(substring); @@ -334,11 +333,11 @@ await Assert.That(text).Matches(pattern); ```csharp // MSTest -Assert.ThrowsException(() => DoSomething()); -await Assert.ThrowsExceptionAsync(() => DoSomethingAsync()); +Assert.Throws(() => DoSomething()); +await Assert.ThrowsAsync(() => DoSomethingAsync()); // TUnit -await Assert.ThrowsAsync(() => DoSomething()); +Assert.Throws(() => DoSomething()); await Assert.ThrowsAsync(() => DoSomethingAsync()); ``` @@ -350,7 +349,7 @@ await Assert.ThrowsAsync(() => DoSomethingAsync()); [TestMethod] [DataRow(1, 2, 3)] [DataRow(10, 20, 30)] -public void AdditionTest(int a, int b, int expected) +public void MSTestAdditionTest(int a, int b, int expected) { Assert.AreEqual(expected, a + b); } @@ -359,7 +358,7 @@ public void AdditionTest(int a, int b, int expected) [Test] [Arguments(1, 2, 3)] [Arguments(10, 20, 30)] -public async Task AdditionTest(int a, int b, int expected) +public async Task TUnitAdditionTest(int a, int b, int expected) { await Assert.That(a + b).IsEqualTo(expected); } @@ -369,13 +368,13 @@ public async Task AdditionTest(int a, int b, int expected) ```csharp // MSTest [TestMethod] -[DynamicData(nameof(TestData), DynamicDataSourceType.Method)] -public void TestMethod(int value, string text) +[DynamicData(nameof(MSTestData), DynamicDataSourceType.Method)] +public void MSTestMethod(int value, string text) { // Test implementation } -private static IEnumerable TestData() +private static IEnumerable MSTestData() { yield return new object[] { 1, "one" }; yield return new object[] { 2, "two" }; @@ -383,13 +382,13 @@ private static IEnumerable TestData() // TUnit [Test] -[MethodDataSource(nameof(TestData))] -public async Task TestMethod(int value, string text) +[MethodDataSource(nameof(TUnitData))] +public async Task TUnitMethod(int value, string text) { // Test implementation } -private static IEnumerable<(int, string)> TestData() +public static IEnumerable<(int, string)> TUnitData() { yield return (1, "one"); yield return (2, "two"); @@ -401,9 +400,9 @@ private static IEnumerable<(int, string)> TestData() ```csharp // MSTest [TestClass] -public class MyTests +public class MSTestContextTests { - public TestContext TestContext { get; set; } + public TestContext TestContext { get; set; } = null!; [TestMethod] public void MyTest() @@ -419,7 +418,7 @@ public class MyTests } // TUnit -public class MyTests +public class TUnitContextTests { [Test] public async Task MyTest(TestContext context) @@ -440,18 +439,18 @@ public class MyTests ```csharp // MSTest [TestMethod] -public void TestWithAttachment() +public void MSTestWithAttachment(TestContext testContext) { // Test logic var logPath = "test-log.txt"; File.WriteAllText(logPath, "test logs"); - TestContext.AddResultFile(logPath); + testContext.AddResultFile(logPath); } // TUnit [Test] -public async Task TestWithAttachment() +public async Task TUnitWithAttachment() { // Test logic var logPath = "test-log.txt"; @@ -495,10 +494,10 @@ Skip.Test("Test is inconclusive"); [TestClass] public class OrderServiceTests { - private static IDatabase _sharedDatabase; - private IOrderService _orderService; + private static ITestDatabase _sharedDatabase = null!; + private IOrderService _orderService = null!; - public TestContext TestContext { get; set; } + public TestContext TestContext { get; set; } = null!; [AssemblyInitialize] public static void AssemblyInit(TestContext context) @@ -543,7 +542,7 @@ public class OrderServiceTests [DynamicData(nameof(GetInvalidOrders), DynamicDataSourceType.Method)] public void CreateOrder_WithInvalidData_ThrowsException(int productId, string productName, double price) { - Assert.ThrowsException(() => + Assert.Throws(() => _orderService.CreateOrder(productId, productName, (decimal)price)); } @@ -589,10 +588,10 @@ public class OrderServiceTests ```csharp public class OrderServiceTests { - private static IDatabase _sharedDatabase = null!; + private static ITestDatabase _sharedDatabase = null!; private IOrderService _orderService = null!; - [Before(Assembly)] + [Before(HookType.Assembly)] public static async Task AssemblyInit() { // Runs once per assembly @@ -635,11 +634,11 @@ public class OrderServiceTests [MethodDataSource(nameof(GetInvalidOrders))] public async Task CreateOrder_WithInvalidData_ThrowsException(int productId, string productName, double price) { - await Assert.ThrowsAsync(() => + Assert.Throws(() => _orderService.CreateOrder(productId, productName, (decimal)price)); } - private static IEnumerable<(int productId, string productName, double price)> GetInvalidOrders() + public static IEnumerable<(int productId, string productName, double price)> GetInvalidOrders() { yield return (0, "Product", 10.00); yield return (1, "", 10.00); @@ -668,7 +667,7 @@ public class OrderServiceTests _sharedDatabase?.Dispose(); } - [After(Assembly)] + [After(HookType.Assembly)] public static async Task AssemblyCleanup() { // Runs once after all tests in assembly @@ -753,7 +752,7 @@ public class CalculatorTests await Assert.That(result).IsEqualTo(expected); } - private static IEnumerable<(int a, int b, int expected)> GetMultiplicationData() +public static IEnumerable<(int a, int b, int expected)> GetMultiplicationData() { yield return (2, 3, 6); yield return (4, 5, 20); @@ -774,7 +773,6 @@ public class TimeoutTests public async Task LongRunningOperation_CompletesInTime() { await Task.Delay(2000); - Assert.IsTrue(true); } } ``` @@ -800,7 +798,7 @@ public class TimeoutTests ### Expected Exception (Obsolete Pattern) **MSTest Code (Old Style):** -```csharp +```text [TestClass] public class ValidationTests { @@ -818,10 +816,10 @@ public class ValidationTests public class ValidationTests { [Test] - public async Task ValidateInput_NullInput_ThrowsException() + public void ValidateInput_NullInput_ThrowsException() { - await Assert.ThrowsAsync(() => - Validator.ValidateInput(null)); + Assert.Throws(() => + ValidateInput(null)); } } ``` @@ -839,12 +837,12 @@ public class ValidationTests [DeploymentItem("testdata.json")] public class FileBasedTests { - public TestContext TestContext { get; set; } + public TestContext TestContext { get; set; } = null!; [TestMethod] public void LoadTestData_ValidFile_Succeeds() { - var filePath = Path.Combine(TestContext.DeploymentDirectory, "testdata.json"); + var filePath = Path.Combine(TestContext.DeploymentDirectory ?? Directory.GetCurrentDirectory(), "testdata.json"); var data = File.ReadAllText(filePath); Assert.IsNotNull(data); } @@ -988,7 +986,7 @@ public async Task AdvancedAssertions_Examples() await Assert.That(value).IsNotEqualTo(0); // String assertions with custom messages - await Assert.That(text).Contains("World").WithMessage("Should contain 'World'"); + await Assert.That(text).Contains("World"); await Assert.That(text).StartsWith("Hello"); await Assert.That(text).EndsWith("!"); await Assert.That(text).Matches(@"^\w+"); @@ -1017,7 +1015,7 @@ public async Task AdvancedAssertions_Examples() [TestClass] public class ContextTests { - public TestContext TestContext { get; set; } + public TestContext TestContext { get; set; } = null!; [TestMethod] public void UsingTestContext_AllProperties() @@ -1033,7 +1031,6 @@ public class ContextTests TestContext.Properties["CustomKey"] = "CustomValue"; var customValue = TestContext.Properties["CustomKey"]; - Assert.IsTrue(true); } [TestMethod] @@ -1062,7 +1059,7 @@ public class ContextTests // Accessing test details context.Output.WriteLine($"Class: {context.Metadata.TestDetails.ClassType.Name}"); - context.Output.WriteLine($"Method: {context.Metadata.TestDetails.MethodInfo.Name}"); + context.Output.WriteLine($"Method: {context.Metadata.TestDetails.MethodName}"); // Accessing attributes and properties var properties = context.Metadata.TestDetails.GetAttributes(); @@ -1071,7 +1068,7 @@ public class ContextTests context.Output.WriteLine($"{prop.Name}: {prop.Value}"); } - await Assert.That(true).IsTrue(); + await Assert.That(context).IsNotNull(); } [Test] diff --git a/docs/docs/migration/nunit.md b/docs/docs/migration/nunit.md index 67ab65d8426..e904b30a44f 100644 --- a/docs/docs/migration/nunit.md +++ b/docs/docs/migration/nunit.md @@ -1,4 +1,3 @@ - # Migrating from NUnit @@ -245,15 +244,15 @@ await Assert.That(value1).IsGreaterThan(value2); ```csharp // NUnit Assert.That(actual, Is.EqualTo(expected)); -Assert.That(value, Is.True); -Assert.That(value, Is.Null); +Assert.That(condition, Is.True); +Assert.That(optional, Is.Null); Assert.That(text, Does.Contain("substring")); Assert.That(collection, Has.Count.EqualTo(5)); // TUnit await Assert.That(actual).IsEqualTo(expected); -await Assert.That(value).IsTrue(); -await Assert.That(value).IsNull(); +await Assert.That(condition).IsTrue(); +await Assert.That(optional).IsNull(); await Assert.That(text).Contains("substring"); await Assert.That(collection).Count().IsEqualTo(5); ``` @@ -262,12 +261,12 @@ await Assert.That(collection).Count().IsEqualTo(5); ```csharp // NUnit -CollectionAssert.AreEqual(expected, actual); +CollectionAssert.AreEqual(values, numbers); CollectionAssert.Contains(collection, item); CollectionAssert.IsEmpty(collection); // TUnit -await Assert.That(actual).IsEquivalentTo(expected); +await Assert.That(numbers).IsEquivalentTo(values); await Assert.That(collection).Contains(item); await Assert.That(collection).IsEmpty(); ``` @@ -294,7 +293,7 @@ Assert.Throws(() => DoSomething()); Assert.ThrowsAsync(async () => await DoSomethingAsync()); // TUnit -await Assert.ThrowsAsync(() => DoSomething()); +Assert.Throws(() => DoSomething()); await Assert.ThrowsAsync(async () => await DoSomethingAsync()); ``` @@ -303,26 +302,26 @@ await Assert.ThrowsAsync(async () => await DoSomethin #### TestCaseSource ```csharp // NUnit -[TestCaseSource(nameof(TestData))] -public void TestMethod(int value, string text) +[TestCaseSource(nameof(NUnitData))] +public void NUnitMethod(int value, string text) { // Test implementation } -private static IEnumerable TestData() +private static IEnumerable NUnitData() { yield return new object[] { 1, "one" }; yield return new object[] { 2, "two" }; } // TUnit -[MethodDataSource(nameof(TestData))] -public async Task TestMethod(int value, string text) +[MethodDataSource(nameof(TUnitData))] +public async Task TUnitMethod(int value, string text) { // Test implementation } -private static IEnumerable<(int, string)> TestData() +public static IEnumerable<(int, string)> TUnitData() { yield return (1, "one"); yield return (2, "two"); @@ -335,7 +334,7 @@ private static IEnumerable<(int, string)> TestData() // NUnit [TestCase(1, 2, 3)] [TestCase(10, 20, 30)] -public void AdditionTest(int a, int b, int expected) +public void NUnitAdditionTest(int a, int b, int expected) { Assert.AreEqual(expected, a + b); } @@ -344,7 +343,7 @@ public void AdditionTest(int a, int b, int expected) [Test] [Arguments(1, 2, 3)] [Arguments(10, 20, 30)] -public async Task AdditionTest(int a, int b, int expected) +public async Task TUnitAdditionTest(int a, int b, int expected) { await Assert.That(a + b).IsEqualTo(expected); } @@ -373,7 +372,7 @@ public void TUnitTest(TUnit.Core.TestContext context) ```csharp // NUnit [Test] -public void TestWithAttachment() +public void NUnitTestWithAttachment() { // Test logic var logPath = "test-log.txt"; @@ -384,7 +383,7 @@ public void TestWithAttachment() // TUnit [Test] -public async Task TestWithAttachment() +public async Task TUnitTestWithAttachment() { // Test logic var logPath = "test-log.txt"; @@ -497,8 +496,8 @@ public class EnvironmentTests(string environment) [TestFixture] public class ProductServiceTests { - private IDatabase _database; - private ProductService _productService; + private ITestDatabase _database = null!; + private ProductService _productService = null!; [OneTimeSetUp] public void OneTimeSetup() @@ -547,7 +546,7 @@ public class ProductServiceTests { yield return new object[] { "", 10.00 }; yield return new object[] { "Product", -5.00 }; - yield return new object[] { null, 10.00 }; + yield return new object[] { null!, 10.00 }; } [TearDown] @@ -570,11 +569,11 @@ public class ProductServiceTests ```csharp public class ProductServiceTests { - private IDatabase _database = null!; - private ProductService _productService = null!; + private static ITestDatabase _database = null!; + private static ProductService _productService = null!; [Before(Class)] - public async Task ClassSetup() + public static async Task ClassSetup() { // Runs once before all tests in the class _database = new InMemoryDatabase(); @@ -613,11 +612,11 @@ public class ProductServiceTests [MethodDataSource(nameof(InvalidProductData))] public async Task CreateProduct_WithInvalidData_ThrowsException(string name, decimal price) { - await Assert.ThrowsAsync( + Assert.Throws( () => _productService.CreateProduct(name, price)); } - private static IEnumerable<(string name, decimal price)> InvalidProductData() + public static IEnumerable<(string name, decimal price)> InvalidProductData() { yield return ("", 10.00m); yield return ("Product", -5.00m); @@ -632,7 +631,7 @@ public class ProductServiceTests } [After(Class)] - public async Task ClassCleanup() + public static async Task ClassCleanup() { // Runs once after all tests in the class _database?.Dispose(); @@ -672,7 +671,7 @@ public async Task ProcessValue_WithRange(int value) await Assert.That(result).IsGreaterThan(0); } -private static IEnumerable GetRange() +public static IEnumerable GetRange() { return Enumerable.Range(1, 10); } @@ -802,14 +801,14 @@ public class AssemblySetup ```csharp public static class AssemblyHooks { - [Before(Assembly)] + [Before(HookType.Assembly)] public static async Task AssemblySetup() { // Initialize resources needed by all tests Console.WriteLine("Assembly setup running"); } - [After(Assembly)] + [After(HookType.Assembly)] public static async Task AssemblyCleanup() { // Cleanup resources diff --git a/docs/docs/migration/testcontext-interface-organization.md b/docs/docs/migration/testcontext-interface-organization.md index 658768dfa23..7e23cbf86c1 100644 --- a/docs/docs/migration/testcontext-interface-organization.md +++ b/docs/docs/migration/testcontext-interface-organization.md @@ -1,4 +1,3 @@ - # TestContext Interface Organization Migration Guide @@ -15,25 +14,20 @@ This migration guide helps you update code that directly accesses `TestContext` `TestContext` now exposes its API through focused interface properties: ```csharp -public partial class TestContext : - ITestExecution, - ITestParallelization, - ITestOutput, - ITestMetadata, - ITestDependencies, - ITestStateBag, - ITestEvents +public static class TestContextInterfaceExample { - // Organized API access through interface properties - public ITestExecution Execution => this; - public ITestParallelization Parallelism => this; - public ITestOutput Output => this; - public ITestMetadata Metadata => this; - public ITestDependencies Dependencies => this; - public ITestStateBag StateBag => this; - public ITestEvents Events => this; - - // Note: Services property is internal - use dependency injection instead + public static void ShowInterfaces(TestContext context) + { + ITestExecution execution = context.Execution; + ITestParallelization parallelism = context.Parallelism; + ITestOutput output = context.Output; + ITestMetadata metadata = context.Metadata; + ITestDependencies dependencies = context.Dependencies; + ITestStateBag stateBag = context.StateBag; + ITestEvents events = context.Events; + + Console.WriteLine($"{execution}, {parallelism}, {output}, {metadata}, {dependencies}, {stateBag}, {events}"); + } } ``` @@ -93,7 +87,7 @@ If you were directly accessing properties on `TestContext`, they now need to be #### Execution-Related Properties **Before:** -```csharp +```text // ❌ Old - Direct access var customExecutor = TestContext.Current.CustomHookExecutor; TestContext.Current.ReportResult = false; @@ -103,15 +97,15 @@ TestContext.Current.AddLinkedCancellationToken(externalToken); **After:** ```csharp // ✅ New - Through Execution interface -var customExecutor = TestContext.Current.Execution.CustomHookExecutor; -TestContext.Current.Execution.ReportResult = false; -TestContext.Current.Execution.AddLinkedCancellationToken(externalToken); +var customExecutor = TestContext.Current!.Execution.CustomHookExecutor; +TestContext.Current!.Execution.ReportResult = false; +TestContext.Current!.Execution.AddLinkedCancellationToken(externalToken); ``` #### Metadata-Related Properties **Before:** -```csharp +```text // ❌ Old - Direct access var formatter = TestContext.Current.DisplayNameFormatter; TestContext.Current.DisplayNameFormatter = typeof(MyFormatter); @@ -120,8 +114,8 @@ TestContext.Current.DisplayNameFormatter = typeof(MyFormatter); **After:** ```csharp // ✅ New - Through Metadata interface -var formatter = TestContext.Current.Metadata.DisplayNameFormatter; -TestContext.Current.Metadata.DisplayNameFormatter = typeof(MyFormatter); +var formatter = TestContext.Current!.Metadata.DisplayNameFormatter; +TestContext.Current!.Metadata.DisplayNameFormatter = typeof(MyFormatter); ``` #### Event Access @@ -129,7 +123,7 @@ TestContext.Current.Metadata.DisplayNameFormatter = typeof(MyFormatter); Events are now accessed directly through the `Events` interface property, and all events are nullable for lazy initialization: **Before:** -```csharp +```text // ❌ Old - Accessing through a nested Events property TestContext.Current.Events.OnTestStart += handler; ``` @@ -137,13 +131,9 @@ TestContext.Current.Events.OnTestStart += handler; **After:** ```csharp // ✅ New - Direct access to nullable event properties -TestContext.Current.Events.OnTestStart += handler; +TestContext.Current!.Events.OnTestStart?.Add(eventHandler, 0); -// Events are nullable and lazily initialized -if (TestContext.Current.Events.OnTestStart != null) -{ - await TestContext.Current.Events.OnTestStart.InvokeAsync(testContext, testContext); -} +// TUnit invokes registered handlers when the event occurs. ``` ### Custom Hook Executors @@ -151,10 +141,10 @@ if (TestContext.Current.Events.OnTestStart != null) If you're implementing custom hook executors that access these properties: **Before:** -```csharp -public class MyHookExecutor : IHookExecutor +```text +public static class LegacyHookExecutorExample { - public async Task ExecuteAsync(TestContext context, Func hookBody) + public static async Task ExecuteAsync(TestContext context, Func hookBody) { // ❌ Old - Direct property access if (context.ReportResult) @@ -167,9 +157,9 @@ public class MyHookExecutor : IHookExecutor **After:** ```csharp -public class MyHookExecutor : IHookExecutor +public static class HookExecutorExample { - public async Task ExecuteAsync(TestContext context, Func hookBody) + public static async Task ExecuteAsync(TestContext context, Func hookBody) { // ✅ New - Through Execution interface if (context.Execution.ReportResult) @@ -185,7 +175,7 @@ public class MyHookExecutor : IHookExecutor If you're setting custom hook executors during test registration: **Before:** -```csharp +```text public class CustomTestBuilder { public void ConfigureTest(TestContext context) @@ -213,7 +203,7 @@ public class CustomTestBuilder ### Cancellation Token Linking **Before:** -```csharp +```text [Before(Test)] public void Setup() { @@ -232,7 +222,7 @@ public void Setup() var externalCts = new CancellationTokenSource(); // ✅ New - Through Execution interface - TestContext.Current.Execution.AddLinkedCancellationToken(externalCts.Token); + TestContext.Current!.Execution.AddLinkedCancellationToken(externalCts.Token); } ``` @@ -242,11 +232,13 @@ public void Setup() IntelliSense now groups related functionality together, making it easier to find what you need: - ```csharp -TestContext.Current.Execution. // Shows only execution-related members -TestContext.Current.Metadata. // Shows only metadata-related members -TestContext.Current.Output. // Shows only output-related members +var current = TestContext.Current!; +ITestExecution execution = current.Execution; +ITestMetadata metadata = current.Metadata; +ITestOutput output = current.Output; + +Console.WriteLine($"{execution}, {metadata}, {output}"); ``` ### 2. Clearer Intent @@ -270,7 +262,7 @@ Consumers can depend on specific interfaces instead of the full `TestContext`: ```csharp // Before: Depends on entire TestContext -public class MyService +public class FocusedService { public void ProcessTest(TestContext context) { } } @@ -391,11 +383,13 @@ public interface ITestParallelization **Important:** The `Limiter` property is **read-only** on the public interface. To set the parallel limiter, use the phase-specific `TestRegisteredContext.SetParallelLimiter()` method during test registration: ```csharp -[TestRegistered] -public static void OnTestRegistered(TestRegisteredContext context) +public sealed class ParallelLimitAttribute : Attribute, ITestRegisteredEventReceiver { - // ✅ Correct - Use phase-specific context - context.SetParallelLimiter(new ParallelLimit3()); + public ValueTask OnTestRegistered(TestRegisteredContext context) + { + context.SetParallelLimiter(new TUnit.Core.Helpers.ProcessorCountParallelLimit()); + return ValueTask.CompletedTask; + } } ``` diff --git a/docs/docs/migration/xunit.md b/docs/docs/migration/xunit.md index a93d75df53b..b45f5ce4f52 100644 --- a/docs/docs/migration/xunit.md +++ b/docs/docs/migration/xunit.md @@ -1,4 +1,3 @@ - # Migrating from xUnit.net @@ -345,6 +344,7 @@ public class MyTests [ClassData(typeof(TestDataGenerator))] public void TestWithClassData(int number, string text) { + Assert.True(number > 0); Assert.NotNull(text); } } @@ -358,6 +358,7 @@ public class MyTests [MethodDataSource(nameof(TestDataGenerator.GetTestData))] public async Task TestWithClassData(int number, string text) { + await Assert.That(number).IsGreaterThan(0); await Assert.That(text).IsNotNull(); } } @@ -481,20 +482,22 @@ public class AsyncSetupTests : IAsyncLifetime { private HttpClient _client = null!; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { _client = new HttpClient(); - await _client.GetAsync("https://api.example.com/warm-up"); + await _client.GetAsync("https://api.example.com/warm-up", Xunit.TestContext.Current.CancellationToken); } [Fact] public async Task FetchData_ReturnsSuccess() { - var response = await _client.GetAsync("https://api.example.com/data"); + var response = await _client.GetAsync( + "https://api.example.com/data", + Xunit.TestContext.Current.CancellationToken); Assert.True(response.IsSuccessStatusCode); } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { _client?.Dispose(); await Task.CompletedTask; @@ -708,7 +711,7 @@ public class UserTests(DatabaseFixture fixture) [Test] public async Task CreateUser_Succeeds() { - // Test using fixture.Connection + _ = fixture.Connection; } } @@ -718,7 +721,7 @@ public class ProductTests(DatabaseFixture fixture) [Test] public async Task CreateProduct_Succeeds() { - // Test using fixture.Connection + _ = fixture.Connection; } } ``` @@ -782,7 +785,7 @@ public class LoggingTests { _output.WriteLine("Starting test"); - var result = PerformOperation(); + var result = CalculateResult(); _output.WriteLine($"Result: {result}"); Assert.True(result > 0); @@ -799,7 +802,7 @@ public class LoggingTests { context.Output.WriteLine("Starting test"); - var result = PerformOperation(); + var result = CalculateResult(); context.Output.WriteLine($"Result: {result}"); await Assert.That(result).IsGreaterThan(0); @@ -832,10 +835,9 @@ public class TestWithAttachments { // Test logic var logPath = "test-log.txt"; - await File.WriteAllTextAsync(logPath, "test logs"); + await File.WriteAllTextAsync(logPath, "test logs", Xunit.TestContext.Current.CancellationToken); - _testContextAccessor.Current!.Attachments.Add( - new FileAttachment(logPath, "Test Log")); + Xunit.TestContext.Current.AddAttachment(logPath, "Test Log"); } } ``` @@ -1058,13 +1060,13 @@ public class UserServiceTests : IClassFixture, IAsyncLifetime _output = output; } - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { _userService = new UserService(_dbFixture.Connection); await _userService.InitializeAsync(); } - public Task DisposeAsync() => Task.CompletedTask; + public ValueTask DisposeAsync() => ValueTask.CompletedTask; [Theory] [InlineData("john@example.com", "John")] diff --git a/docs/docs/reference/command-line-flags.md b/docs/docs/reference/command-line-flags.md index 2e86de5f3e6..ef2d60f2ca9 100644 --- a/docs/docs/reference/command-line-flags.md +++ b/docs/docs/reference/command-line-flags.md @@ -137,7 +137,8 @@ Please note that for the coverage and trx report, you need to install [additiona --report-html (Deprecated) The HTML report is now generated by default. - Disable it with the TUNIT_DISABLE_HTML_REPORTER environment variable. + Disable it with TUNIT_DISABLE_HTML_REPORTER or set + context.Settings.Reporting.HtmlReportEnabled = false. --report-html-filename Path for the HTML test report file diff --git a/docs/docs/reference/environment-variables.md b/docs/docs/reference/environment-variables.md index 0f32f77ac35..f78d7e2b142 100644 --- a/docs/docs/reference/environment-variables.md +++ b/docs/docs/reference/environment-variables.md @@ -60,9 +60,11 @@ Accepts truthy values: `true`, `1`, `yes` (case-insensitive). **Use case:** When you don't need the HTML report or want to reduce disk I/O. The report is written to `TestResults/{AssemblyName}-report.html` by default. +**Programmatic equivalent:** `context.Settings.Reporting.HtmlReportEnabled = false` + ### TUNIT_DISABLE_ARTIFACT_UPLOAD -Skips the autmoatic upload of the html report but still generates the files. +Skips automatic upload of the HTML report but still generates the files. ```bash export TUNIT_DISABLE_ARTIFACT_UPLOAD=true @@ -77,6 +79,20 @@ GitHub changed the server-side behaviour of `upload-artifacts` since v4 and hasn Forgejo and gitea don't implement this new artifact endpoint but the runners set `GITHUB_ACTIONS=true`. In this case it attempts to upload the report a few times until it eventually backs off after 30s. +**Programmatic equivalent:** `context.Settings.Reporting.ArtifactUploadEnabled = false` + +### TUNIT_DISABLE_JSON_REPORT + +Disables the machine-readable JSON sidecar written alongside the HTML report. + +```bash +export TUNIT_DISABLE_JSON_REPORT=true +``` + +Accepts truthy values: `true`, `1`, `yes` (case-insensitive). + +**Programmatic equivalent:** `context.Settings.Reporting.JsonReportEnabled = false` + ### TUNIT_DISABLE_JUNIT_REPORTER Disables the JUnit XML reporter. @@ -284,8 +300,9 @@ When the same setting is configured in multiple places, TUnit follows this prior | `TUNIT_DISABLE_GITHUB_REPORTER` | - | Disables GitHub reporter | | `TUNIT_DISABLE_JUNIT_REPORTER` | - | Disables JUnit reporter | | `TUNIT_ENABLE_JUNIT_REPORTER` | - | Enables JUnit reporter | -| `TUNIT_DISABLE_HTML_REPORTER` | - | Disables HTML report generation | -| `TUNIT_DISABLE_ARTIFACT_UPLOAD` | - | Keeps the HTML report file but skips the GitHub Actions artifact upload | +| `TUNIT_DISABLE_HTML_REPORTER` | - | Disables HTML report generation (`context.Settings.Reporting.HtmlReportEnabled = false`) | +| `TUNIT_DISABLE_JSON_REPORT` | - | Disables the machine-readable JSON sidecar (`context.Settings.Reporting.JsonReportEnabled = false`) | +| `TUNIT_DISABLE_ARTIFACT_UPLOAD` | - | Keeps the HTML report file but skips the GitHub Actions artifact upload (`context.Settings.Reporting.ArtifactUploadEnabled = false`) | | `JUNIT_XML_OUTPUT_PATH` | - | JUnit output path | | `TUNIT_MAX_PARALLEL_TESTS` | `--maximum-parallel-tests` | Max parallel tests | | `TUNIT_EXECUTION_MODE` | `--reflection` | Selects source-generation (`sourcegeneration`/`aot`) or `reflection` execution mode | diff --git a/docs/docs/reference/programmatic-configuration.md b/docs/docs/reference/programmatic-configuration.md index 07ab47d89ae..ad9ee85c755 100644 --- a/docs/docs/reference/programmatic-configuration.md +++ b/docs/docs/reference/programmatic-configuration.md @@ -2,7 +2,6 @@ sidebar_position: 4 --- - # Programmatic Configuration @@ -16,6 +15,7 @@ Settings are organized into logical groups: - `Parallelism` — concurrent test execution limits - `Execution` — runtime behavior such as fail-fast - `Display` — output and display options +- `Reporting` — HTML report generation and publishing - `Mocks` — defaults for TUnit.Mocks when the package is referenced ## Usage @@ -34,6 +34,7 @@ public class TestSetup context.Settings.Timeouts.DefaultTestTimeout = TimeSpan.FromMinutes(5); context.Settings.Timeouts.DefaultHookTimeout = TimeSpan.FromMinutes(2); context.Settings.Execution.FailFast = true; + context.Settings.Reporting.HtmlReportEnabled = false; context.Settings.Mocks.DefaultMode = MockBehavior.Strict; return Task.CompletedTask; @@ -74,6 +75,16 @@ Settings are accessed exclusively through `context.Settings` in the discovery ho |---|---|---|---| | `FailFast` | `bool` | `false` | Cancels the remaining test run after the first test failure. | +### `context.Settings.Reporting` + +| Property | Type | Default | Description | +|---|---|---|---| +| `HtmlReportEnabled` | `bool` | `true` | Generates the HTML test report. | +| `JsonReportEnabled` | `bool` | `true` | Generates the machine-readable JSON sidecar used by report aggregation. | +| `ArtifactUploadEnabled` | `bool` | `true` | Uploads the HTML report as an artifact when supported by the CI environment. | + +The corresponding `TUNIT_DISABLE_HTML_REPORTER`, `TUNIT_DISABLE_JSON_REPORT`, and `TUNIT_DISABLE_ARTIFACT_UPLOAD` environment variables take precedence over these values. + ### `context.Settings.Mocks` Available when `TUnit.Mocks` is referenced. @@ -95,9 +106,11 @@ When the same setting is configured in multiple places, the following priority o Your test project sets a conservative parallelism limit in code: - ```csharp -context.Settings.Parallelism.MaximumParallelTests = 1; +public static void Configure(BeforeTestDiscoveryContext context) +{ + context.Settings.Parallelism.MaximumParallelTests = 1; +} ``` A developer on a powerful machine can override this for a local run without changing code: diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index 45b36ff3fc8..5b7a991b801 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -110,13 +110,23 @@ dotnet test --treenode-filter "/*/*/*/*[Category=Integration][Priority=High]" If you see trim warnings or "source generator did not generate" errors, make sure you're using AOT-compatible data sources: - +Replace reflection-based `MethodDataSource(typeof(DataClass), "GetData")` usage with the generic form: + ```csharp -// Reflection-based — may cause AOT issues -[MethodDataSource(typeof(DataClass), "GetData")] +public sealed class DataClass +{ + public static IEnumerable GetData() => [1, 2, 3]; +} -// AOT-friendly generic version -[MethodDataSource(nameof(DataClass.GetData))] +public class DataSourceTests +{ + [Test] + [MethodDataSource(nameof(DataClass.GetData))] + public void ReceivesData(int value) + { + Console.WriteLine(value); + } +} ``` ## InstanceMethodDataSource Returns No Tests @@ -125,7 +135,6 @@ If you're using `InstanceMethodDataSource` with a `ClassDataSource` fixture that The fix is to return predefined identifiers that don't depend on initialisation: - ```csharp public class Fixture : IAsyncInitializer { @@ -137,22 +146,23 @@ public class Fixture : IAsyncInitializer } public IEnumerable GetTestCaseIds() => TestCaseIds; + + private static Task StartDockerContainerAsync() => Task.CompletedTask; } ``` ## Hooks Not Running -Class-level and assembly-level hooks must be static: +Class-level and assembly-level hooks must be static. An instance declaration such as `public void ClassSetup()` is invalid: - ```csharp -// Won't work — instance method -[Before(Class)] -public void IncorrectClassSetup() { } - -// Works -[Before(Class)] -public static void ClassSetup() { } +public class HookExamples +{ + [Before(Class)] + public static void ClassSetup() + { + } +} ``` Test-level hooks (`[Before(Test)]` / `[After(Test)]`) can be instance methods. diff --git a/docs/docs/writing-tests/aot.md b/docs/docs/writing-tests/aot.md index e14d1de98f7..e6afe9888eb 100644 --- a/docs/docs/writing-tests/aot.md +++ b/docs/docs/writing-tests/aot.md @@ -62,6 +62,7 @@ public class GenericTestClass var value = default(T); await Assert.That(input).IsEqualTo("test data"); + await Assert.That(value).IsEqualTo(default(T)); // Can use both generic type T and regular parameters } } @@ -255,31 +256,22 @@ AOT mode provides helpful compile-time diagnostics for common issues: ### Generic Test Diagnostics - -```csharp -// ❌ This will generate TUnit0058 error -[Test] -public async Task GenericTest() // Missing [GenerateGenericTest] -{ - var value = default(T); - await Assert.That(value).IsNotNull().Or.IsNull(); -} +Omitting `[GenerateGenericTest]` produces diagnostic `TUnit0058`. Supply each concrete type explicitly: -// ✅ Correct usage +```csharp [Test] [GenerateGenericTest(typeof(int))] [GenerateGenericTest(typeof(string))] public async Task GenericTest() { var value = default(T); - await Assert.That(value).IsNotNull().Or.IsNull(); + await Assert.That(value).IsEqualTo(default(T)); } ``` ### Data Source Diagnostics - ```csharp public class DataSourceDiagnostics { @@ -294,7 +286,9 @@ public class DataSourceDiagnostics public IEnumerable GetDynamicData() { // This method uses reflection internally - not AOT compatible - return SomeReflectionBasedDataGenerator.GetData(); + return System.Reflection.Assembly.GetExecutingAssembly() + .GetTypes() + .Select(type => new object[] { type }); } // ✅ Use static, compile-time known data sources diff --git a/docs/docs/writing-tests/artifacts.md b/docs/docs/writing-tests/artifacts.md index 8e023a24e0d..11db596f854 100644 --- a/docs/docs/writing-tests/artifacts.md +++ b/docs/docs/writing-tests/artifacts.md @@ -1,4 +1,3 @@ - # Test Artifacts @@ -18,7 +17,6 @@ Use `TestContext.ResultsDirectory` to place generated files alongside reports an artifacts. The property returns the absolute directory selected by Microsoft.Testing.Platform, including any `--results-directory` override. - ```csharp [Test] public async Task CaptureLog() @@ -34,7 +32,6 @@ public async Task CaptureLog() The simplest way to attach an artifact is by providing just the file path: - ```csharp [Test] public async Task MyIntegrationTest() @@ -58,7 +55,6 @@ public async Task MyIntegrationTest() For more control, you can create an `Artifact` object directly: - ```csharp [Test] public async Task MyIntegrationTest() @@ -113,7 +109,6 @@ public class MyTests You can attach multiple artifacts to a single test: - ```csharp [Test] public async Task ComplexIntegrationTest() @@ -153,7 +148,6 @@ Attach files to the entire test session using `TestSessionContext.Current.AddArt ### Basic Usage - ```csharp [Before(TestSession)] public static void SetupTestSession() @@ -176,7 +170,6 @@ public static void SetupTestSession() Attach configuration files to document the test environment: - ```csharp [Before(TestSession)] public static void DocumentTestEnvironment() @@ -206,7 +199,6 @@ public static void DocumentTestEnvironment() Generate and attach performance reports for the entire test session: - ```csharp [After(TestSession)] public static void GeneratePerformanceReport() @@ -247,7 +239,6 @@ public class Artifact Consider cleaning up temporary artifact files after test execution to avoid accumulating files: - ```csharp [After(TestSession)] public static void CleanupArtifacts() @@ -264,7 +255,6 @@ public static void CleanupArtifacts() Create a unique directory for each test's artifacts: - ```csharp [Before(Test)] public void SetupTestArtifactDirectory() @@ -280,7 +270,8 @@ public void SetupTestArtifactDirectory() [Test] public void MyTest() { - var artifactDir = (string)TestContext.Current!.StateBag["ArtifactDir"]; + var artifactDir = TestContext.Current!.StateBag["ArtifactDir"] as string + ?? throw new InvalidOperationException("ArtifactDir was not initialized"); var logPath = Path.Combine(artifactDir, "test.log"); // ... test logic ... @@ -297,12 +288,12 @@ public void MyTest() For large artifacts (videos, extensive logs), consider only attaching them when tests fail: - ```csharp [After(Test)] public async Task ConditionalArtifactAttachment() { - var testContext = TestContext.Current; + var testContext = TestContext.Current + ?? throw new InvalidOperationException("No active test context."); if (testContext?.Execution.Result?.State is TestState.Failed or TestState.Timeout) { @@ -323,7 +314,6 @@ public async Task ConditionalArtifactAttachment() Provide clear, descriptive names and descriptions for your artifacts: - ```csharp // ❌ Not descriptive TestContext.Current!.Output.AttachArtifact(new Artifact @@ -345,7 +335,6 @@ TestContext.Current!.Output.AttachArtifact(new Artifact Always ensure the file exists before attaching: - ```csharp var logPath = "path/to/logfile.log"; @@ -367,14 +356,14 @@ else ### Browser Testing with Playwright - ```csharp [After(Test)] public async Task CapturePlaywrightArtifacts() { - var testContext = TestContext.Current; + var testContext = TestContext.Current + ?? throw new InvalidOperationException("No active test context."); - if (testContext?.Execution.Result?.State != TestState.Passed) + if (testContext.Execution.Result?.State != TestState.Passed) { // Capture screenshot var screenshotPath = $"artifacts/screenshot-{testContext.Id}.png"; @@ -387,7 +376,7 @@ public async Task CapturePlaywrightArtifacts() }); // Capture video if enabled - if (_browserContext.Options?.RecordVideo != null) + if (_page.Video is not null) { await _page.CloseAsync(); var videoPath = await _page.Video!.PathAsync(); @@ -404,7 +393,6 @@ public async Task CapturePlaywrightArtifacts() ### API Testing - ```csharp [Test] public async Task ApiIntegrationTest() @@ -440,7 +428,6 @@ public async Task ApiIntegrationTest() ### Database Testing - ```csharp [Test] public async Task DatabaseIntegrationTest() diff --git a/docs/docs/writing-tests/class-data-source.md b/docs/docs/writing-tests/class-data-source.md index b43b68b7402..c93514c40ff 100644 --- a/docs/docs/writing-tests/class-data-source.md +++ b/docs/docs/writing-tests/class-data-source.md @@ -4,6 +4,50 @@ 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. +The type created by `ClassDataSource` must have a public parameterless constructor. Constructor injection is supported on the test class receiving the data source, but not on the data source type itself. + +For nested dependencies, use property injection on the data source type: + +```csharp +[ClassDataSource(Shared = SharedType.PerTestSession)] +public class ApplicationTests(ApplicationFixture fixture) +{ + [Test] + public async Task Application_Is_Available() + { + await Assert.That(fixture.IsAvailable).IsTrue(); + } +} + +public class ApplicationFixture : IAsyncInitializer +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required DatabaseFixture Database { get; init; } + + public bool IsAvailable { get; private set; } + + public Task InitializeAsync() + { + // Database has already been initialized. + IsAvailable = Database.IsAvailable; + return Task.CompletedTask; + } +} + +public class DatabaseFixture : IAsyncInitializer +{ + public bool IsAvailable { get; private set; } + + public Task InitializeAsync() + { + IsAvailable = true; + return Task.CompletedTask; + } +} +``` + +See [Nested Property Injection](property-injection.md#nested-property-injection) for dependency chains and lifecycle details. + 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. 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. @@ -49,9 +93,16 @@ If you are using an overload that supports injecting multiple classes at once (e E.g. - ```csharp -[Test] +public sealed record Value1; +public sealed record Value2; +public sealed record Value3; +public sealed record Value4; +public sealed record Value5; + +public class MyType +{ + [Test] [ClassDataSource ( Shared = [SharedType.PerTestSession, SharedType.Keyed, SharedType.PerClass, SharedType.Keyed, SharedType.None], @@ -62,8 +113,9 @@ E.g. // Index 3: Value4 (Keyed) - "Value4Key" // Index 4: Value5 (None) - empty string (no key needed) )] - public class MyType(Value1 value1, Value2 value2, Value3 value3, Value4 value4, Value5 value5) + public void Test(Value1 value1, Value2 value2, Value3 value3, Value4 value4, Value5 value5) { - + Console.WriteLine($"{value1}, {value2}, {value3}, {value4}, {value5}"); } +} ``` diff --git a/docs/docs/writing-tests/data-driven-overview.md b/docs/docs/writing-tests/data-driven-overview.md index 3ec029621a4..7a9a2bf09e0 100644 --- a/docs/docs/writing-tests/data-driven-overview.md +++ b/docs/docs/writing-tests/data-driven-overview.md @@ -45,7 +45,6 @@ public static IEnumerable GetCases() => ["hello", "world"]; ### Class data source (shared fixture) - ```csharp [ClassDataSource(Shared = SharedType.PerTestSession)] public class MyTests(DatabaseFixture db) diff --git a/docs/docs/writing-tests/dependency-injection.md b/docs/docs/writing-tests/dependency-injection.md index bb7d79a774d..442ad71cf32 100644 --- a/docs/docs/writing-tests/dependency-injection.md +++ b/docs/docs/writing-tests/dependency-injection.md @@ -1,4 +1,5 @@ - + + # Dependency Injection @@ -29,6 +30,7 @@ public class MyTestClass(SomeDependency dep) public async Task MyTest() { // dep was provided by CustomConstructor.Create() + await Assert.That(dep).IsNotNull(); } } ``` diff --git a/docs/docs/writing-tests/event-subscribing.md b/docs/docs/writing-tests/event-subscribing.md index da714f02447..43cccf5be23 100644 --- a/docs/docs/writing-tests/event-subscribing.md +++ b/docs/docs/writing-tests/event-subscribing.md @@ -1,4 +1,3 @@ - # Event Subscribing @@ -57,14 +56,14 @@ Use `EventReceiverStage.Late` (the default) when your event receiver needs to: ```csharp public class DatabaseConnectionAttribute : Attribute, ITestStartEventReceiver { - private IDbConnection? _connection; + private DbConnection? _connection; // Execute before [Before(Test)] hooks so the connection is available to them public EventReceiverStage Stage => EventReceiverStage.Early; public async ValueTask OnTestStart(TestContext context) { - _connection = new SqlConnection(connectionString); + _connection = new NpgsqlConnection(connectionString); await _connection.OpenAsync(); // Store connection in test context for use by hooks and test diff --git a/docs/docs/writing-tests/explicit.md b/docs/docs/writing-tests/explicit.md index e699446850c..f81d1f5cab5 100644 --- a/docs/docs/writing-tests/explicit.md +++ b/docs/docs/writing-tests/explicit.md @@ -1,4 +1,3 @@ - # Explicit diff --git a/docs/docs/writing-tests/generic-attributes.md b/docs/docs/writing-tests/generic-attributes.md index 62b10edcc91..af17dd59981 100644 --- a/docs/docs/writing-tests/generic-attributes.md +++ b/docs/docs/writing-tests/generic-attributes.md @@ -1,4 +1,3 @@ - # Generic Attributes @@ -27,7 +26,7 @@ public class CalculatorTests [MethodDataSource(nameof(TestDataProviders.AdditionTestCases))] public async Task Add_ShouldReturnCorrectSum(int a, int b, int expected) { - var result = Calculator.Add(a, b); + var result = new Calculator().Add(a, b); await Assert.That(result).IsEqualTo(expected); } } @@ -58,6 +57,15 @@ public class DatabaseFixture : IAsyncInitializer, IAsyncDisposable { await Connection.DisposeAsync(); } + + private static Task OpenConnectionAsync() => + throw new NotImplementedException(); +} + +public static class DatabaseQueryExtensions +{ + public static Task QueryUserAsync(this DbConnection connection, int id) => + Task.FromResult(new User { Id = id, Name = "Alice" }); } public class UserRepositoryTests @@ -90,7 +98,7 @@ public class UserTests [MethodDataSource(nameof(UserTestData.All))] public async Task ValidateUser_ShouldPass(User user) { - var isValid = await UserValidator.ValidateAsync(user); + var isValid = await Task.FromResult(!string.IsNullOrEmpty(user.Name)); await Assert.That(isValid).IsTrue(); } } @@ -199,12 +207,26 @@ public class DatabaseUsersAttribute : AsyncDataSourceGeneratorAttribute } } +public sealed class DatabaseContext : IDisposable +{ + public IQueryable Users => Array.Empty().AsQueryable(); + + public void Dispose() + { + } +} + +public enum Permission +{ + FullAccess +} + // Usage [Test] [DatabaseUsers("Admin")] public async Task AdminUser_ShouldHaveFullPermissions(User adminUser) { - var permissions = await GetUserPermissions(adminUser); + var permissions = await Task.FromResult(new[] { Permission.FullAccess }); await Assert.That(permissions).Contains(Permission.FullAccess); } ``` @@ -243,8 +265,8 @@ public async Task ValidateUser(User user) ```csharp public interface ITestScenario { - TInput Input { get; } - TExpected Expected { get; } + TInput Input { get; set; } + TExpected Expected { get; set; } } public class CalculationScenario : ITestScenario<(int, int), int> @@ -256,10 +278,12 @@ public class CalculationScenario : ITestScenario<(int, int), int> public class ScenarioDataSource : TypedDataSourceAttribute where TScenario : ITestScenario<(int, int), int>, new() { - public override IEnumerable GetData() + public override async IAsyncEnumerable>> GetTypedDataRowsAsync( + DataGeneratorMetadata dataGeneratorMetadata) { - yield return new TScenario { Input = (1, 2), Expected = 3 }; - yield return new TScenario { Input = (5, 5), Expected = 10 }; + yield return () => Task.FromResult(new TScenario { Input = (1, 2), Expected = 3 }); + yield return () => Task.FromResult(new TScenario { Input = (5, 5), Expected = 10 }); + await Task.CompletedTask; } } @@ -268,7 +292,7 @@ public class ScenarioDataSource : TypedDataSourceAttribute public async Task TestCalculation(CalculationScenario scenario) { var (a, b) = scenario.Input; - var result = Calculator.Add(a, b); + var result = new Calculator().Add(a, b); await Assert.That(result).IsEqualTo(scenario.Expected); } ``` @@ -279,7 +303,7 @@ public async Task TestCalculation(CalculationScenario scenario) The following code **WILL NOT COMPILE** due to error CS8968: -```csharp +```text // ❌ This does NOT work - CS8968 error public abstract class EntityTestBase where TEntity : IEntity @@ -302,6 +326,8 @@ public abstract class EntityTestBase protected abstract TEntity CreateEntity(TId id); protected abstract Task GetEntityAsync(TId id); + protected virtual Task SaveEntityAsync(TEntity entity) => Task.CompletedTask; + // ✅ This works - instance method data source [Test] [InstanceMethodDataSource(nameof(GetTestIds))] @@ -352,6 +378,8 @@ public abstract class EntityTestBase protected abstract TEntity CreateEntity(TId id); protected abstract Task GetEntityAsync(TId id); + protected virtual Task SaveEntityAsync(TEntity entity) => Task.CompletedTask; + protected async Task Entity_ShouldBeRetrievable(TId id) { var entity = CreateEntity(id); @@ -362,12 +390,11 @@ public abstract class EntityTestBase } } -// Concrete base class for Guid-based entities -public abstract class GuidEntityTestBase : EntityTestBase - where TEntity : IEntity +// Concrete base class for User entities +public abstract class UserEntityTestBase : EntityTestBase { [Test] - [MethodDataSource>(nameof(GetTestIds))] + [MethodDataSource(nameof(GetTestIds))] public async Task TestEntity(Guid id) { await Entity_ShouldBeRetrievable(id); @@ -381,13 +408,13 @@ public abstract class GuidEntityTestBase : EntityTestBase +public class UserEntityTests : UserEntityTestBase { protected override User CreateEntity(Guid id) => new User { Id = id, Name = "Test User" }; protected override Task GetEntityAsync(Guid id) => - UserRepository.GetByIdAsync(id); + Task.FromResult(new User { Id = id, Name = "Test User" }); } ``` @@ -405,7 +432,8 @@ public class ReflectiveDataSource<[DynamicallyAccessedMembers( DynamicallyAccessedMemberTypes.PublicProperties)] T> : TypedDataSourceAttribute where T : new() { - public override IEnumerable GetData() + public override async IAsyncEnumerable>> GetTypedDataRowsAsync( + DataGeneratorMetadata dataGeneratorMetadata) { var type = typeof(T); var properties = type.GetProperties(); @@ -415,8 +443,10 @@ public class ReflectiveDataSource<[DynamicallyAccessedMembers( { var instance = new T(); // Set property values... - yield return instance; + yield return () => Task.FromResult(instance); } + + await Task.CompletedTask; } } ``` @@ -425,13 +455,23 @@ public class ReflectiveDataSource<[DynamicallyAccessedMembers( ### 1. Use Generic Attributes for Type Safety - +Prefer `[MethodDataSource(nameof(DataProvider.GetData))]` over the reflection-based non-generic form: + ```csharp -// ❌ Non-generic - prone to errors -[MethodDataSource(typeof(DataProvider), "GetData")] +public sealed class DataProvider +{ + public static IEnumerable GetData() => [1, 2, 3]; +} -// ✅ Generic - compile-time safety -[MethodDataSource(nameof(DataProvider.GetData))] +public class GenericDataSourceTests +{ + [Test] + [MethodDataSource(nameof(DataProvider.GetData))] + public void ReceivesData(int value) + { + Console.WriteLine(value); + } +} ``` ### 2. Leverage Constraints @@ -440,11 +480,18 @@ public class ReflectiveDataSource<[DynamicallyAccessedMembers( public class ValidatableDataSource : TypedDataSourceAttribute where T : IValidatable { - public override IEnumerable GetData() + public override async IAsyncEnumerable>> GetTypedDataRowsAsync( + DataGeneratorMetadata dataGeneratorMetadata) { - // Only return valid instances - return GenerateInstances().Where(x => x.IsValid()); + foreach (var instance in GenerateInstances().Where(x => x.IsValid())) + { + yield return () => Task.FromResult(instance); + } + + await Task.CompletedTask; } + + private static IEnumerable GenerateInstances() => []; } ``` @@ -455,11 +502,16 @@ public abstract class JsonFileDataSource : TypedDataSourceAttribute { protected abstract string FilePath { get; } - public override IEnumerable GetData() + public override async IAsyncEnumerable>> GetTypedDataRowsAsync( + DataGeneratorMetadata dataGeneratorMetadata) { var json = File.ReadAllText(FilePath); - return JsonSerializer.Deserialize>(json) - ?? Enumerable.Empty(); + foreach (var item in JsonSerializer.Deserialize>(json) ?? []) + { + yield return () => Task.FromResult(item); + } + + await Task.CompletedTask; } } @@ -477,7 +529,7 @@ public class UserJsonDataSource : JsonFileDataSource /// /// The type to deserialize CSV rows into. /// Must have a parameterless constructor. -public class CsvDataSource : TypedDataSourceAttribute +public abstract class CsvDataSource : TypedDataSourceAttribute where T : new() { // Implementation diff --git a/docs/docs/writing-tests/hooks.md b/docs/docs/writing-tests/hooks.md index 923b526343c..cae5ddf164d 100644 --- a/docs/docs/writing-tests/hooks.md +++ b/docs/docs/writing-tests/hooks.md @@ -1,4 +1,3 @@ - # Hooks @@ -10,7 +9,6 @@ For the full execution order, see [Test Lifecycle](lifecycle.md). Hook methods can be synchronous or asynchronous: - ```csharp [Before(Test)] public void SynchronousSetup() // ✅ Valid @@ -44,7 +42,6 @@ public async Task AsyncCleanup() // ✅ Valid Hooks can optionally accept a context object and/or a `CancellationToken`: - ```csharp [Before(Test)] public async Task Setup(TestContext context, CancellationToken cancellationToken) @@ -74,7 +71,6 @@ public async Task Setup(TestContext context, CancellationToken cancellationToken A common pattern in `[After]` hooks is checking whether the test failed: - ```csharp [After(Test)] public async Task Cleanup(TestContext context, CancellationToken cancellationToken) @@ -195,7 +191,7 @@ public class MyTestClass [After(Test)] public async Task AfterEachTest() { - await new HttpClient().GetAsync($"https://localhost/test-finished-notifier?testName={TestContext.Current.Metadata.TestName}"); + await new HttpClient().GetAsync($"https://localhost/test-finished-notifier?testName={TestContext.Current!.Metadata.TestName}"); } [Test] @@ -217,7 +213,6 @@ public class MyTestClass Setting `AsyncLocal` values in `[Before]` hooks is supported. Call `context.AddAsyncLocalValues()` to propagate them into the test framework: - ```csharp [BeforeEvery(Class)] public static void BeforeClass(ClassHookContext context) diff --git a/docs/docs/writing-tests/mocking/advanced.md b/docs/docs/writing-tests/mocking/advanced.md index 83220d8e302..a1c2e4c6d95 100644 --- a/docs/docs/writing-tests/mocking/advanced.md +++ b/docs/docs/writing-tests/mocking/advanced.md @@ -2,7 +2,6 @@ sidebar_position: 5 --- - # Advanced Features @@ -36,7 +35,7 @@ Trigger an event automatically when a method is called using the typed `.Raises{ ```csharp mock.SendMessage(Any()) - .RaisesOnMessage("echo"); + .RaisesOnMessage(mock.Object, "echo"); mock.Object.SendMessage("test"); // OnMessage event fires with "echo" @@ -150,6 +149,12 @@ cannot even write the type name: // Inside the Azure Functions Worker SDK — not your code: // features.Get() // IFunctionBindingsFeature is internal to the SDK +public interface IInvocationFeatures +{ + T? Get(); +} + +// Usage var features = IInvocationFeatures.Mock(); // The SDK's internal Get() call receives a functional runtime stub instead of null. @@ -188,15 +193,15 @@ Manage multiple mocks with shared behavior and batch operations: var repo = new MockRepository(MockBehavior.Strict); var serviceMock = repo.Of(); -var loggerMock = repo.Of(); +var greeterMock = repo.Of(); // Configure each mock individually -serviceMock.GetData(Any()).Returns("result"); -loggerMock.Log(Any()); +serviceMock.GetUser(Any()).Returns(user); +greeterMock.Greet(Any()).Returns("hello"); // Exercise code -serviceMock.Object.GetData(1); -loggerMock.Object.Log("hello"); +_ = serviceMock.Object.GetUser(1); +_ = greeterMock.Object.Greet("Alice"); // Batch verification repo.VerifyAll(); // all setups invoked across all mocks @@ -224,6 +229,9 @@ repo.Reset(); // clear all mocks Get a diagnostic report of setup coverage and call matching: ```csharp +var mock = Mock.Of(); +var svc = mock.Object; + mock.GetUser(Any()).Returns(new User("Alice")); mock.Delete(Any()); @@ -231,10 +239,10 @@ svc.GetUser(1); // Delete was never called var diag = mock.GetDiagnostics(); -diag.TotalSetups; // 2 -diag.ExercisedSetups; // 1 -diag.UnusedSetups; // [Delete(Any())] -diag.UnmatchedCalls; // [] (all calls matched a setup) +_ = diag.TotalSetups; // 2 +_ = diag.ExercisedSetups; // 1 +_ = diag.UnusedSetups; // [Delete(Any())] +_ = diag.UnmatchedCalls; // [] (all calls matched a setup) ``` Useful for debugging why a mock isn't behaving as expected, or for finding dead setups. @@ -277,7 +285,7 @@ svc.GetUser(1); mock.Reset(); svc.GetUser(1); // returns default (setup cleared) -mock.Invocations.Count; // 0 (history cleared) +_ = mock.Invocations.Count; // 0 (history cleared) ``` The `SetupAllProperties()` flag is preserved across resets. @@ -308,6 +316,8 @@ nameable, source-generator mocked, with fully typed setups, matchers, and verifi `InternalsVisibleTo` is required from the target assembly: ```csharp +var features = IInvocationFeatures.Mock(); +var myResult = new object(); var bindings = IFunctionBindingsFeature.Mock(); // internal to the SDK bindings.InvocationResult.Returns(myResult); diff --git a/docs/docs/writing-tests/mocking/argument-matchers.md b/docs/docs/writing-tests/mocking/argument-matchers.md index 200fc192a72..db9a969962d 100644 --- a/docs/docs/writing-tests/mocking/argument-matchers.md +++ b/docs/docs/writing-tests/mocking/argument-matchers.md @@ -2,7 +2,6 @@ sidebar_position: 4 --- - # Argument Matchers diff --git a/docs/docs/writing-tests/mocking/http.md b/docs/docs/writing-tests/mocking/http.md index 6989350ea24..746d3ead747 100644 --- a/docs/docs/writing-tests/mocking/http.md +++ b/docs/docs/writing-tests/mocking/http.md @@ -2,7 +2,6 @@ sidebar_position: 6 --- - # HTTP Mocking @@ -14,7 +13,6 @@ dotnet add package TUnit.Mocks.Http ## Getting Started - ```csharp using TUnit.Mocks; @@ -39,18 +37,19 @@ public async Task Fetches_Users_From_Api() `Mock.HttpClient()` returns a `MockHttpClient` — a subclass of `HttpClient` with a `.Handler` property for configuring setups and verifying calls: - ```csharp // With base address (most common) -using var client = Mock.HttpClient("https://api.example.com"); +using var clientWithBaseAddress = Mock.HttpClient("https://api.example.com"); +_ = clientWithBaseAddress; // Without base address -using var client = Mock.HttpClient(); -client.BaseAddress = new Uri("https://api.example.com"); +using var clientWithoutBaseAddress = Mock.HttpClient(); +clientWithoutBaseAddress.BaseAddress = new Uri("https://api.example.com"); // Just the handler (when you need more control) var handler = Mock.HttpHandler(); -using var client = handler.CreateClient("https://api.example.com"); +using var handlerClient = handler.CreateClient("https://api.example.com"); +_ = handlerClient; ``` `MockHttpClient` **is** an `HttpClient` — pass it anywhere `HttpClient` is expected. Use `.Handler` for all setup and verification: @@ -61,7 +60,6 @@ All setup is done through `client.Handler` (or directly on a `MockHttpHandler` i ### By HTTP Method - ```csharp client.Handler.OnGet("/api/users").RespondWithJson("""[{"id": 1}]"""); client.Handler.OnPost("/api/users").Respond(HttpStatusCode.Created); @@ -71,7 +69,6 @@ client.Handler.OnDelete("/api/users/1").Respond(HttpStatusCode.NoContent); ### Any Request - ```csharp client.Handler.OnAnyRequest().Respond(HttpStatusCode.OK); ``` @@ -80,7 +77,6 @@ client.Handler.OnAnyRequest().Respond(HttpStatusCode.OK); Use `OnRequest` with a fluent matcher for complex conditions: - ```csharp // Match by path prefix client.Handler.OnRequest(r => r.Method(HttpMethod.Get).PathStartsWith("/api/v2")) @@ -123,7 +119,6 @@ client.Handler.OnRequest(r => r.Matching(msg => msg.RequestUri?.Port == 8080)) ### Basic Responses - ```csharp // Status code only client.Handler.OnGet("/health").Respond(HttpStatusCode.OK); @@ -139,7 +134,6 @@ client.Handler.OnGet("/api/version").RespondWithString("1.0.0"); For more control, use the response builder: - ```csharp client.Handler.OnGet("/api/data") .Respond(HttpStatusCode.OK) @@ -151,7 +145,6 @@ client.Handler.OnGet("/api/data") Build responses based on the incoming request: - ```csharp client.Handler.OnPost("/api/echo") .Respond() @@ -167,7 +160,6 @@ client.Handler.OnPost("/api/echo") ### Simulating Delays - ```csharp client.Handler.OnGet("/api/slow") .Respond(HttpStatusCode.OK) @@ -176,7 +168,6 @@ client.Handler.OnGet("/api/slow") ### Throwing Exceptions - ```csharp client.Handler.OnGet("/api/failing") .Throws("Connection refused"); @@ -189,7 +180,6 @@ client.Handler.OnGet("/api/timeout") Return different responses for successive requests to the same endpoint: - ```csharp var setup = client.Handler.OnGet("/api/status"); setup.RespondWithString("starting"); @@ -205,7 +195,6 @@ setup.Then().RespondWithString("complete"); By default, unmatched requests return **404 Not Found**. You can change this: - ```csharp // Change default status code client.Handler.WithDefaultStatus(HttpStatusCode.ServiceUnavailable); @@ -218,7 +207,6 @@ client.Handler.ThrowOnUnmatched(); ### Verify Call Count - ```csharp client.Handler.Verify(r => r.Method(HttpMethod.Get).Path("/api/users"), Times.Once); client.Handler.Verify(r => r.Method(HttpMethod.Delete), Times.Never); @@ -226,14 +214,12 @@ client.Handler.Verify(r => r.Method(HttpMethod.Delete), Times.Never); ### Verify No Unmatched Requests - ```csharp client.Handler.VerifyNoUnmatchedRequests(); ``` ### Inspect Captured Requests - ```csharp await Assert.That(client.Handler.Requests).Count().IsEqualTo(2); await Assert.That(client.Handler.Requests[0].Method).IsEqualTo(HttpMethod.Get); @@ -258,7 +244,6 @@ Each `CapturedRequest` provides: `Mock.HttpClientFactory()` returns a factory whose `CreateClient` produces non-disposing `HttpClient`s sharing one `MockHttpHandler`, so captured requests survive `using` blocks in the system under test. - ```csharp var factory = Mock.HttpClientFactory().WithBaseAddress("https://api.example.com"); factory.Handler.OnGet("/api/users").RespondWithJson("""[{"id":1}]"""); @@ -273,7 +258,6 @@ factory.Handler.Verify(r => r.Method(HttpMethod.Get).Path("/api/users"), Times.O For typed/named clients registered via `services.AddHttpClient("users")`, assign a dedicated handler (and optionally base address) per name. Name lookups are case-insensitive, matching `IHttpClientFactory` semantics. Unregistered names fall back to `factory.Handler`. - ```csharp var factory = Mock.HttpClientFactory() .WithHandler("users", Mock.HttpHandler()) @@ -287,7 +271,6 @@ factory.HandlerFor("orders").OnPost("/").Respond(HttpStatusCode.Created); ## Reset - ```csharp client.Handler.Reset(); // clears all setups and captured requests ``` diff --git a/docs/docs/writing-tests/mocking/index.md b/docs/docs/writing-tests/mocking/index.md index 1de27cf1335..ab9ce1cd5fe 100644 --- a/docs/docs/writing-tests/mocking/index.md +++ b/docs/docs/writing-tests/mocking/index.md @@ -2,7 +2,6 @@ sidebar_position: 1 --- - # TUnit.Mocks @@ -67,7 +66,6 @@ public class GreeterTests The `Mock.Of()` factory is also available as an alternative syntax: - ```csharp var mock = Mock.Of(); // equivalent to IGreeter.Mock() ``` @@ -90,7 +88,6 @@ var mock = Mock.Of(); // equivalent to IGreeter.Mock() All factory methods accept an optional `MockBehavior` parameter: - ```csharp var loose = IService.Mock(); // loose (default) var strict = IService.Mock(MockBehavior.Strict); // throws on unconfigured calls @@ -116,7 +113,6 @@ public class GlobalSetup With this setting, `IService.Mock()`, `Mock.Of()`, `Mock.Wrap(instance)`, `Mock.OfDelegate()`, and `new MockRepository()` use strict mode by default. Passing a `MockBehavior` still overrides the global default: - ```csharp var strict = IService.Mock(); // uses global strict default var loose = IService.Mock(MockBehavior.Loose); // explicit override @@ -126,13 +122,12 @@ var loose = IService.Mock(MockBehavior.Loose); // explicit override `T.Mock()` returns a `Mock` wrapper (for interfaces, a generated subclass that also implements the interface). Extension methods are generated directly on `Mock` for each member of the mocked type, and the chain methods (`.Returns()`, `.WasCalled()`, etc.) disambiguate between setup and verification: - ```csharp var mock = IService.Mock(); mock.GetUser(Any()).Returns(user); // setup — .Returns() makes it a stub mock.GetUser(42).WasCalled(Times.Once); // verify — .WasCalled() makes it a check -mock.RaiseOnMessage("hi"); // raise events — Raise{EventName}() +mock.RaiseOnMessage(mock.Object, "hi"); // raise events — Raise{EventName}() _ = mock.Object; // the T instance (also available via direct cast) ``` @@ -140,9 +135,9 @@ _ = mock.Object; // the T instance (also available vi For interfaces, `IMyInterface.Mock()` (a C# 14 static extension member) returns a specialized wrapper type that extends `Mock` **and** implements the interface directly. This means the mock can be used anywhere the interface is expected — no `.Object` or cast needed: - ```csharp var mock = IGreeter.Mock(); +static void AcceptGreeter(IGreeter greeter) { } // mock IS an IGreeter — assign directly, pass to methods, use in collections IGreeter greeter = mock; @@ -156,7 +151,6 @@ mock.Greet("Alice").WasCalled(); `T.Mock()` is the recommended syntax for all types — interfaces, abstract classes, and concrete classes. For interfaces it returns a typed wrapper; for classes it returns `Mock`. Constructor arguments are supported as strongly-typed parameters: - ```csharp var strict = IGreeter.Mock(MockBehavior.Strict); var service = MyService.Mock("connectionString", 42); @@ -170,7 +164,6 @@ var service = MyService.Mock("connectionString", 42); `Mock` also supports implicit conversion to `T` — so `T.Mock()` works without `.Object`: - ```csharp var mock = IGreeter.Mock(); IGreeter greeter = mock; // implicit conversion @@ -187,7 +180,6 @@ IGreeter greeter = mock; // implicit conversion TUnit.Mocks imports matchers globally — no `Arg.` prefix needed. Raw values, inline lambdas, and `Any()` work directly as arguments: - ```csharp var mock = IUserService.Mock(); @@ -199,10 +191,10 @@ mock.GetUser(42).Returns(alice); // Inline lambdas — predicate matching directly in the call mock.GetUser(id => id > 0).Returns(validUser); -mock.GetByRole(role => role == "admin").Returns(admins); +mock.GetByRole(role => role == "admin").Returns(users); // Mix lambdas with Any() or raw values -mock.Search(name => name.StartsWith("A"), Any()).Returns(results); +mock.Search(name => name.StartsWith("A"), Any()).Returns(users); // Is() — explicit predicate matching (also works) mock.GetUser(Is(id => id > 0)).Returns(validUser); diff --git a/docs/docs/writing-tests/mocking/logging.md b/docs/docs/writing-tests/mocking/logging.md index a326a34c62e..7b35110ffb9 100644 --- a/docs/docs/writing-tests/mocking/logging.md +++ b/docs/docs/writing-tests/mocking/logging.md @@ -2,7 +2,6 @@ sidebar_position: 7 --- - # Logging @@ -18,7 +17,6 @@ Unlike `TUnit.Mocks`, the logging helpers are plain classes — no source genera ## Getting Started - ```csharp using TUnit.Mocks; using Microsoft.Extensions.Logging; @@ -34,36 +32,34 @@ public async Task Service_Logs_On_Startup() service.Start(); // Assert - logger.VerifyLog(LogLevel.Information, "started", Times.Once); + logger.VerifyLog(Microsoft.Extensions.Logging.LogLevel.Information, "started", Times.Once); } ``` ## Creating a Logger - ```csharp // Untyped logger -var logger = Mock.Logger(); -ILogger iLogger = logger; +var untypedLogger = Mock.Logger(); +Microsoft.Extensions.Logging.ILogger untypedILogger = untypedLogger; // With category name -var logger = Mock.Logger("MyApp.Services"); +var categoryLogger = Mock.Logger("MyApp.Services"); // Generic typed logger (implements ILogger) -var logger = Mock.Logger(); -ILogger iLogger = logger; +var typedLogger = Mock.Logger(); +Microsoft.Extensions.Logging.ILogger typedILogger = typedLogger; ``` ## Inspecting Entries - ```csharp logger.LogInformation("User {UserId} logged in", 42); logger.LogWarning("Disk space low"); // All entries await Assert.That(logger.Entries).Count().IsEqualTo(2); -await Assert.That(logger.Entries[0].LogLevel).IsEqualTo(LogLevel.Information); +await Assert.That(logger.Entries[0].LogLevel).IsEqualTo(Microsoft.Extensions.Logging.LogLevel.Information); await Assert.That(logger.Entries[0].Message).Contains("42"); // Most recent entry @@ -88,10 +84,9 @@ Each `LogEntry` provides: Build verification queries with filters: - ```csharp // By level -logger.VerifyLog().AtLevel(LogLevel.Error).WasCalled(Times.Once); +logger.VerifyLog().AtLevel(Microsoft.Extensions.Logging.LogLevel.Error).WasCalled(Times.Once); // By message content (contains) logger.VerifyLog().ContainingMessage("failed").WasCalled(Times.Once); @@ -104,7 +99,7 @@ logger.VerifyLog().WithException().WasCalled(Times.On // Combined filters logger.VerifyLog() - .AtLevel(LogLevel.Error) + .AtLevel(Microsoft.Extensions.Logging.LogLevel.Error) .WithException() .ContainingMessage("database") .WasCalled(Times.Once); @@ -112,16 +107,15 @@ logger.VerifyLog() ### Shorthand Methods - ```csharp // Verify message at level (at least once) -logger.VerifyLog(LogLevel.Error, "connection failed"); +logger.VerifyLog(Microsoft.Extensions.Logging.LogLevel.Error, "connection failed"); // Verify message at level with count -logger.VerifyLog(LogLevel.Warning, "retry", Times.Exactly(3)); +logger.VerifyLog(Microsoft.Extensions.Logging.LogLevel.Warning, "retry", Times.Exactly(3)); // Verify nothing logged at a level -logger.VerifyNoLog(LogLevel.Error); +logger.VerifyNoLog(Microsoft.Extensions.Logging.LogLevel.Error); // Verify nothing logged at all logger.VerifyNoLogs(); @@ -129,33 +123,30 @@ logger.VerifyNoLogs(); ### Never Called - ```csharp -logger.VerifyLog().AtLevel(LogLevel.Error).WasNeverCalled(); +logger.VerifyLog().AtLevel(Microsoft.Extensions.Logging.LogLevel.Error).WasNeverCalled(); ``` ## Filtering Entries Retrieve entries matching specific criteria: - ```csharp // By level -var errors = logger.GetLogs(LogLevel.Error); +var errors = logger.GetLogs(Microsoft.Extensions.Logging.LogLevel.Error); // By message content var retryLogs = logger.GetLogs("retry"); // Using the fluent API var matching = logger.VerifyLog() - .AtLevel(LogLevel.Warning) + .AtLevel(Microsoft.Extensions.Logging.LogLevel.Warning) .ContainingMessage("timeout") .GetMatchingEntries(); ``` ## Reset - ```csharp logger.Clear(); // removes all captured entries ``` @@ -164,7 +155,6 @@ logger.Clear(); // removes all captured entries Pass `Mock.Logger()` anywhere `ILogger` is expected: - ```csharp [Test] public async Task OrderService_Logs_Errors() @@ -172,10 +162,10 @@ public async Task OrderService_Logs_Errors() var logger = Mock.Logger(); var service = new OrderService(logger); - await service.ProcessOrder(invalidOrder); + service.ProcessOrder(invalidOrder); logger.VerifyLog() - .AtLevel(LogLevel.Error) + .AtLevel(Microsoft.Extensions.Logging.LogLevel.Error) .ContainingMessage("validation failed") .WasCalled(Times.Once); } diff --git a/docs/docs/writing-tests/mocking/setup.md b/docs/docs/writing-tests/mocking/setup.md index 26b8b1d75f4..ff24e6f7e67 100644 --- a/docs/docs/writing-tests/mocking/setup.md +++ b/docs/docs/writing-tests/mocking/setup.md @@ -2,7 +2,6 @@ sidebar_position: 2 --- - # Setup & Stubbing @@ -12,7 +11,6 @@ Methods are called directly on `Mock` — the chain method (`.Returns()`, `.T ### Return Values - ```csharp // Fixed return value mock.GetUser(Any()).Returns(new User("Alice")); @@ -27,7 +25,6 @@ mock.GetUserAsync(Any()).Returns(new User("Alice")); ### Throwing Exceptions - ```csharp // Throw a specific exception type mock.Delete(Any()).Throws(); @@ -38,23 +35,21 @@ mock.Delete(Any()).Throws(new ArgumentException("bad id")); ### Callbacks - ```csharp // Simple callback var callCount = 0; -mock.Process(Any()) +mock.Process(Any()) .Callback(() => callCount++); // Callback with access to arguments -mock.Process(Any()) - .Callback((object?[] args) => Console.WriteLine($"Called with: {args[0]}")); +mock.Process(Any()) + .Callback(() => Console.WriteLine("Called with an argument")); ``` ### Sequential Behaviors Use `.Then()` to define different behaviors for successive calls: - ```csharp mock.GetValue(Any()) .Throws() // 1st call: throws @@ -72,7 +67,6 @@ mock.GetValue(Any()) Chained setup behaviors without `.Then()` run together as a single invocation step. When multiple return behaviors are chained in one step, the last return wins: - ```csharp mock.GetValue(Any()) .Returns("first") @@ -87,7 +81,6 @@ different invocation. Void methods support `Callback` and `Throws` (but not `Returns`): - ```csharp mock.Log(Any()) .Callback(() => { /* side effect */ }); @@ -104,7 +97,6 @@ TUnit.Mocks uses C# 14 extension properties for a natural property API. The defa ### Getter Setup - ```csharp // These are equivalent — both configure the getter mock.Name.Returns("Alice"); @@ -113,7 +105,6 @@ mock.Name.Getter.Returns("Alice"); All method setup operations work on getters: - ```csharp mock.Name.Throws(); mock.Name.Callback(() => Console.WriteLine("Name accessed")); @@ -122,7 +113,6 @@ mock.Name.ReturnsSequentially("first", "second"); ### Setter Setup - ```csharp // React to any value being set mock.Count.Setter.Callback(() => Console.WriteLine("Count was set")); @@ -138,7 +128,6 @@ mock.Name.Setter.Throws(); Call `SetupAllProperties()` to make properties behave like real auto-properties — setters store values, getters return them: - ```csharp var mock = IEntity.Mock(); mock.SetupAllProperties(); @@ -156,8 +145,9 @@ Explicit setups take precedence over auto-tracked values. **Out parameters** are excluded from setup signatures. Use the generated strongly-typed `.SetsOut{Name}()` methods to assign their values: - ```csharp +IEntity svc = mock.Object; + // Strongly-typed — named after the parameter, compile-time safe mock.TryGet("key") .Returns(true) @@ -169,8 +159,9 @@ bool found = svc.TryGet("key", out var value); **Ref parameters** are included in setup signatures and participate in argument matching. Use `.SetsRef{Name}()` to assign output values: - ```csharp +IEntity svc = mock.Object; + mock.Swap(Any()) .SetsRefValue(99); @@ -203,7 +194,6 @@ mock.Object.Multiply(2, 3); // 99 (mocked) Pass constructor arguments for non-default constructors: - ```csharp var mock = MyService.Mock("connectionString", 42); ``` @@ -214,19 +204,19 @@ Interfaces that have static abstract members (directly or inherited) cannot be u Use `[assembly: GenerateMock(typeof(T))]` to work around this. The source generator produces a bridge interface (suffixed `_Mockable`) that provides default implementations for the static abstract members: - ```csharp using TUnit.Mocks; -[assembly: GenerateMock(typeof(IMyParseable))] +[assembly: GenerateMock(typeof(IMyService))] -public interface IMyParseable : IParsable +public interface IMyService { + static abstract string CreateDefaultName(); string Format(); } // In your test — use the generated bridge type: -var mock = Mock.Of(); +var mock = IMyServiceMockable.Mock(); mock.Format().Returns("formatted"); ``` @@ -236,7 +226,6 @@ The bridge type implements all the non-static members of the original interface, Mock any delegate type: - ```csharp var mock = Mock.OfDelegate>(); mock.Invoke(Any()).Returns(42); @@ -251,7 +240,6 @@ Works with `Action<>`, `Func<>`, and custom delegate types. Wrap a real instance to selectively override methods while delegating unconfigured calls to the real implementation: - ```csharp var realService = new ProductionService(); var mock = Mock.Wrap(realService); @@ -267,12 +255,11 @@ mock.Object.DoWork(); // calls realService.DoWork() Create a single mock that implements multiple interfaces: - ```csharp -var mock = Mock.Of(); +var mock = Mock.Of(); -mock.Log(Any()); // ILogger method -mock.Object.Log("test"); +mock.Greet(Any()).Returns("Hello!"); // IGreeter method +_ = mock.Object.Greet("Alice"); ((IDisposable)mock.Object).Dispose(); // IDisposable method ``` @@ -281,24 +268,32 @@ Supports up to 4 interfaces: `Mock.Of()`. Members of the secondary interfaces appear directly on the mock, just like the primary's — setup, verify, and event raising all work the same way: - ```csharp -var mock = Mock.Of(); +var mock = Mock.Of(); -mock.IsDisposed.Returns(true); // IDisposable property setup -mock.Dispose().WasCalled(); // IDisposable verification +mock.Name.Returns("Alice"); // IEntity property setup +mock.Name.WasCalled(); // IEntity property verification ``` When a secondary member's name collides with a member of another interface on the mock, it is exposed with a short interface prefix instead (e.g. `mock.IDisposable_Tag`). -The primary type can also be a concrete class — useful for types like EF Core's `DbContext` that implement infrastructure interfaces explicitly: +The primary type can also be a concrete class: - ```csharp -var mock = Mock.Of>(); -mock.Instance.Returns(serviceProvider); +public class ConcreteService +{ + public virtual string GetName() => "real"; +} -((IInfrastructure)mock.Object).Instance; // serviceProvider +public interface IExtra +{ + string Tag { get; } +} + +var mock = Mock.Of(); +mock.Tag.Returns("test"); + +_ = ((IExtra)mock.Object).Tag; // "test" ``` Constructor arguments for class primaries are supported: `Mock.Of(arg1, arg2)`. @@ -307,8 +302,14 @@ Constructor arguments for class primaries are supported: `Mock.Of ```csharp +public interface IProcessor +{ + event EventHandler? ProcessCompleted; + bool Process(string input); +} + +var mock = Mock.Of(); mock.Process(Any()) .Returns(true) .RaisesProcessCompleted(EventArgs.Empty) // strongly-typed auto-raise event diff --git a/docs/docs/writing-tests/mocking/verification.md b/docs/docs/writing-tests/mocking/verification.md index e5b99412d1c..d33ce57dac5 100644 --- a/docs/docs/writing-tests/mocking/verification.md +++ b/docs/docs/writing-tests/mocking/verification.md @@ -2,7 +2,6 @@ sidebar_position: 3 --- - # Verification @@ -10,7 +9,6 @@ Verification uses the same methods as setup — the chain method (`.WasCalled()` ## Basic Verification - ```csharp // Verify a method was called at least once mock.GetUser(42).WasCalled(); @@ -36,7 +34,6 @@ mock.Delete(Any()).WasNeverCalled(); ### Custom Failure Messages - ```csharp mock.GetUser(42).WasCalled(Times.Once, "GetUser should be called once during initialization"); mock.Delete(Any()).WasNeverCalled("Delete should not be called in read-only mode"); @@ -46,11 +43,9 @@ mock.Delete(Any()).WasNeverCalled("Delete should not be called in read-only mode Property verification mirrors the setup API — defaults to the **getter**: - ```csharp // Getter verification mock.Name.WasCalled(Times.Once); // getter called once -mock.Name.Getter.WasCalled(Times.Once); // explicit — same as above mock.Name.WasNeverCalled(); // getter never accessed // Setter verification — any value @@ -59,14 +54,13 @@ mock.Count.Setter.WasNeverCalled(); // Setter verification — specific value mock.Count.Set(42).WasCalled(Times.Once); -mock.Count.Set(v => v > 0).WasCalled(Times.AtLeast(1)); +mock.Count.Set(Is(v => v > 0)).WasCalled(Times.AtLeast(1)); ``` ## Argument Matching in Verification Verification uses the same `Arg` matchers as setup: - ```csharp // Exact value mock.GetUser(42).WasCalled(Times.Once); @@ -84,7 +78,6 @@ See [Argument Matchers](argument-matchers) for the full list of matchers. Verify calls occurred in a specific order **across one or more mocks**: - ```csharp Mock.VerifyInOrder(() => { @@ -104,7 +97,6 @@ If calls occurred out of order, `VerifyInOrder` throws with a message showing th Verify that **every setup** was invoked at least once: - ```csharp mock.GetUser(Any()).Returns(new User("Alice")); mock.Delete(Any()); @@ -121,7 +113,6 @@ If any setup was never called, `VerifyAll` throws listing the uninvoked setups. Verify that all recorded calls have been explicitly verified: - ```csharp svc.GetUser(1); svc.Delete(2); @@ -138,7 +129,6 @@ If there are unverified calls, `VerifyNoOtherCalls` throws listing them. Use TUnit's `Assert.That` pipeline for assertion-style verification with better error messages: - ```csharp using TUnit.Mocks.Assertions; @@ -155,7 +145,6 @@ This integrates with TUnit's assertion engine — failures appear as assertion e Access the raw call history for custom inspection: - ```csharp var calls = mock.Invocations; diff --git a/docs/docs/writing-tests/nested-data-sources.md b/docs/docs/writing-tests/nested-data-sources.md index a2fa4a5536f..6d497bc7b46 100644 --- a/docs/docs/writing-tests/nested-data-sources.md +++ b/docs/docs/writing-tests/nested-data-sources.md @@ -2,7 +2,8 @@ sidebar_position: 7 --- - + + # Nested Data Sources with Initialization @@ -22,6 +23,8 @@ This typically leads to complex setup code with manual initialization chains. TUnit automatically initializes nested data sources in the correct order using any data source attribute that implements `IDataSourceAttribute` (such as `[ClassDataSource]`). +Declare nested data sources on properties. Constructor-injected dependencies inside a data source type are not currently supported because `ClassDataSource` requires that type to have a public parameterless constructor. + ## Basic Example Here's a complete example of setting up integration tests with Redis and WebApplicationFactory: @@ -41,7 +44,7 @@ public class RedisTestContainer : IAsyncInitializer, IAsyncDisposable public RedisTestContainer() { - _container = new RedisBuilder() + _container = new RedisBuilder("redis:8.2") .WithImage("redis:7-alpine") .Build(); } @@ -67,6 +70,8 @@ public class TestApplication : IAsyncInitializer, IAsyncDisposable public required RedisTestContainer Redis { get; init; } public HttpClient Client { get; private set; } = null!; + public IServiceProvider Services => _factory?.Services + ?? throw new InvalidOperationException("The application has not been initialized."); public async Task InitializeAsync() { @@ -109,7 +114,7 @@ public class UserApiTests response.EnsureSuccessStatusCode(); // Verify the user was cached in Redis - var services = app.Client.Services; + var services = app.Services; var redis = services.GetRequiredService(); var cached = await redis.GetDatabase().StringGetAsync("user:john@example.com"); @@ -161,7 +166,23 @@ public class CompleteTestEnvironment : IAsyncInitializer, IAsyncDisposable } // ... configuration methods + + private static void ConfigureRedis(IServiceCollection services) { } + private static void ConfigureDatabase(IServiceCollection services) { } + private static void ConfigureAwsServices(IServiceCollection services) { } + private static Task SeedTestData() => Task.CompletedTask; + + public async ValueTask DisposeAsync() + { + if (_factory is not null) + { + await _factory.DisposeAsync(); + } + } } + +public sealed class PostgresTestContainer { } +public sealed class LocalStackContainer { } ``` ## Sharing Resources @@ -187,7 +208,7 @@ public class OrderApiTests } // Or share with a specific key for fine-grained control across multiple test classes -public class UserApiTests +public class SharedUserApiTests { [Test] [ClassDataSource(Shared = SharedType.Keyed, Key = "integration-tests")] @@ -230,11 +251,12 @@ public async Task InitializeAsync() // Run migrations after container starts using var connection = new NpgsqlConnection(ConnectionString); - await connection.ExecuteAsync(@" + await using var command = new NpgsqlCommand(@" CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL - )"); + )", connection); + await command.ExecuteNonQueryAsync(); } ``` diff --git a/docs/docs/writing-tests/ordering.md b/docs/docs/writing-tests/ordering.md index eef6e34dfdb..cd8811a0ae7 100644 --- a/docs/docs/writing-tests/ordering.md +++ b/docs/docs/writing-tests/ordering.md @@ -1,4 +1,3 @@ - # Test Ordering & Dependencies @@ -59,7 +58,6 @@ If you have multiple tests with the same name, but different parameter types, th ::: e.g.: - ```csharp public void Test1(string value1, int value2) { ... } @@ -71,7 +69,6 @@ This means you can create more complex test suites, without having to compromise For example, performing some operations on a database and asserting a count at the end: - ```csharp [Test] public async Task AddUser1() @@ -147,7 +144,6 @@ Use argument values or other properties to select the specific test context you Example: - ```csharp [Test] public async Task AddItemToBag() @@ -161,7 +157,8 @@ public async Task AddItemToBag() public async Task DeleteItemFromBag() { var addToBagTestContext = TestContext.Current!.Dependencies.GetTests(nameof(AddItemToBag)).First(); - var itemId = addToBagTestContext.StateBag.Items["ItemId"]; + var itemId = addToBagTestContext.StateBag.Items["ItemId"] + ?? throw new InvalidOperationException("The item ID was not recorded."); await DeleteFromBag(itemId); } ``` @@ -170,7 +167,6 @@ public async Task DeleteItemFromBag() If your test depends on another test, by default, if that dependency fails, then your test that depends on it will not start. This can be bypassed by adding the property `ProceedOnFailure = true` to the `DependsOnAttribute`. Your test suite will still fail due to that test, but it allows you to proceed with other tests if you require it. For example, CRUD testing, and wanting to perform a delete after all your other tests, regardless of if they passed. - ```csharp [Test] public async Task Test1() diff --git a/docs/docs/writing-tests/property-injection.md b/docs/docs/writing-tests/property-injection.md index aa0a7ec1a78..4f3ba50b361 100644 --- a/docs/docs/writing-tests/property-injection.md +++ b/docs/docs/writing-tests/property-injection.md @@ -1,4 +1,5 @@ - + + # Property Injection @@ -95,7 +96,7 @@ public class TestDataFixture : IAsyncDiscoveryInitializer, IAsyncDisposable public async Task InitializeAsync() { // Runs during DISCOVERY, before test enumeration - _testCases = await LoadTestCasesFromDatabaseAsync(); + _testCases = [.. await LoadTestCasesFromDatabaseAsync()]; } public IEnumerable GetTestCases() => _testCases; @@ -158,7 +159,7 @@ public class PropertySetterTests public required InnerModel Property6 { get; init; } // Source-generated data injection - [DataSourceGeneratorTests.AutoFixtureGenerator] + [Arguments("generated_value")] public required string Property7 { get; init; } // Async initialization example (IAsyncInitializer) @@ -209,15 +210,14 @@ Here's a comprehensive example showing how to orchestrate multiple test containe // In-memory SQL container that auto-starts and stops public class InMemorySql : IAsyncInitializer, IAsyncDisposable { - private TestcontainersContainer? _container; + private IContainer? _container; - public TestcontainersContainer Container => _container + public IContainer Container => _container ?? throw new InvalidOperationException("Container not initialized"); public async Task InitializeAsync() { - _container = new TestcontainersBuilder() - .WithImage("postgres:latest") + _container = new ContainerBuilder("postgres:18") .WithEnvironment("POSTGRES_PASSWORD", "password") .Build(); @@ -319,6 +319,12 @@ public class ConditionalService : IAsyncInitializer } } } + +public class DatabaseService +{ + public Task RequiresMigration() => Task.FromResult(false); + public Task MigrateAsync() => Task.CompletedTask; +} ``` #### Circular Dependencies @@ -330,12 +336,16 @@ public class ServiceA : IAsyncInitializer { [ClassDataSource] public required ServiceB B { get; init; } // This will fail! + + public Task InitializeAsync() => Task.CompletedTask; } public class ServiceB : IAsyncInitializer { [ClassDataSource] public required ServiceA A { get; init; } // Circular dependency! + + public Task InitializeAsync() => Task.CompletedTask; } ``` diff --git a/docs/docs/writing-tests/skip.md b/docs/docs/writing-tests/skip.md index 985e63ffd13..6eef171f995 100644 --- a/docs/docs/writing-tests/skip.md +++ b/docs/docs/writing-tests/skip.md @@ -1,4 +1,3 @@ - # Skipping Tests @@ -35,7 +34,6 @@ public class WindowsOnlyAttribute() : SkipAttribute("This test is only supported } ``` - ```csharp using TUnit.Core; diff --git a/docs/docs/writing-tests/test-context.md b/docs/docs/writing-tests/test-context.md index 8c4786f6b5d..20c016a53ab 100644 --- a/docs/docs/writing-tests/test-context.md +++ b/docs/docs/writing-tests/test-context.md @@ -230,7 +230,7 @@ public static IEnumerable TestData() public void MyTest(string value) { // Access the data stored during generation - var generatedAt = TestContext.Current.StateBag["DataGeneratedAt"]; + var generatedAt = TestContext.Current!.StateBag["DataGeneratedAt"]; var version = TestContext.Current.StateBag["GeneratorVersion"]; Console.WriteLine($"Data was generated at: {generatedAt}"); diff --git a/docs/package.json b/docs/package.json index f2aaa2ccc7a..1cc1e39fe02 100644 --- a/docs/package.json +++ b/docs/package.json @@ -51,9 +51,9 @@ "lodash-es": "4.18.1", "minimatch": "10.2.6", "express/path-to-regexp": "8.4.2", - "serialize-javascript": "7.1.0", + "serialize-javascript": "7.1.1", "brace-expansion": "5.0.9", - "qs": "6.15.3", + "qs": "6.16.0", "dompurify": "3.4.14", "node-forge": "1.4.0", "picomatch": "4.0.7", diff --git a/docs/static/benchmarks/AsyncTests.json b/docs/static/benchmarks/AsyncTests.json index 800785581e4..a6b4f81eb5a 100644 --- a/docs/static/benchmarks/AsyncTests.json +++ b/docs/static/benchmarks/AsyncTests.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-08-23T00:20:42.432Z", + "timestamp": "2026-08-30T00:32:59.981Z", "category": "AsyncTests", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", @@ -9,51 +9,51 @@ "results": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "358.9 ms", - "Error": "3.57 ms", - "StdDev": "2.79 ms", - "Median": "358.6 ms" + "Version": "1.65.68", + "Mean": "388.5 ms", + "Error": "7.96 ms", + "StdDev": "22.97 ms", + "Median": "386.4 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "577.1 ms", - "Error": "9.43 ms", - "StdDev": "9.26 ms", - "Median": "574.3 ms" + "Mean": "714.3 ms", + "Error": "12.96 ms", + "StdDev": "19.79 ms", + "Median": "707.6 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "678.0 ms", - "Error": "13.43 ms", - "StdDev": "20.11 ms", - "Median": "673.8 ms" + "Mean": "664.8 ms", + "Error": "6.87 ms", + "StdDev": "5.74 ms", + "Median": "664.0 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "737.0 ms", - "Error": "13.67 ms", - "StdDev": "27.93 ms", + "Mean": "730.9 ms", + "Error": "14.38 ms", + "StdDev": "16.56 ms", "Median": "733.0 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "118.5 ms", - "Error": "1.19 ms", - "StdDev": "1.06 ms", - "Median": "118.6 ms" + "Version": "1.65.68", + "Mean": "116.0 ms", + "Error": "0.31 ms", + "StdDev": "0.29 ms", + "Median": "116.0 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "120.1 ms", - "Error": "1.32 ms", - "StdDev": "1.23 ms", - "Median": "120.0 ms" + "Mean": "118.5 ms", + "Error": "0.81 ms", + "StdDev": "0.68 ms", + "Median": "118.5 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/BuildTime.json b/docs/static/benchmarks/BuildTime.json index ca474a48586..bc49130d080 100644 --- a/docs/static/benchmarks/BuildTime.json +++ b/docs/static/benchmarks/BuildTime.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-08-23T00:20:42.433Z", + "timestamp": "2026-08-30T00:32:59.983Z", "category": "BuildTime", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", @@ -9,35 +9,35 @@ "results": [ { "Method": "Build_TUnit", - "Version": "1.65.38", - "Mean": "917.6 ms", - "Error": "16.66 ms", - "StdDev": "26.90 ms", - "Median": "918.9 ms" + "Version": "1.65.68", + "Mean": "923.9 ms", + "Error": "18.16 ms", + "StdDev": "34.98 ms", + "Median": "915.4 ms" }, { "Method": "Build_NUnit", "Version": "4.6.1", - "Mean": "893.9 ms", - "Error": "13.98 ms", - "StdDev": "12.39 ms", - "Median": "899.1 ms" + "Mean": "884.6 ms", + "Error": "11.22 ms", + "StdDev": "9.95 ms", + "Median": "885.1 ms" }, { "Method": "Build_MSTest", "Version": "4.3.3", - "Mean": "1,036.7 ms", - "Error": "14.72 ms", - "StdDev": "13.77 ms", - "Median": "1,037.3 ms" + "Mean": "1,026.0 ms", + "Error": "20.50 ms", + "StdDev": "47.11 ms", + "Median": "1,019.4 ms" }, { "Method": "Build_xUnit3", "Version": "4.0.0", - "Mean": "862.9 ms", - "Error": "14.21 ms", - "StdDev": "12.60 ms", - "Median": "861.3 ms" + "Mean": "879.1 ms", + "Error": "9.05 ms", + "StdDev": "8.46 ms", + "Median": "881.5 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/DataDrivenTests.json b/docs/static/benchmarks/DataDrivenTests.json index c122dac587c..5124015eff7 100644 --- a/docs/static/benchmarks/DataDrivenTests.json +++ b/docs/static/benchmarks/DataDrivenTests.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-08-23T00:20:42.432Z", + "timestamp": "2026-08-30T00:32:59.981Z", "category": "DataDrivenTests", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", @@ -9,51 +9,51 @@ "results": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "268.40 ms", - "Error": "2.883 ms", - "StdDev": "2.408 ms", - "Median": "267.52 ms" + "Version": "1.65.68", + "Mean": "281.32 ms", + "Error": "5.561 ms", + "StdDev": "11.233 ms", + "Median": "278.96 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "498.74 ms", - "Error": "9.322 ms", - "StdDev": "9.573 ms", - "Median": "496.12 ms" + "Mean": "564.15 ms", + "Error": "10.186 ms", + "StdDev": "16.735 ms", + "Median": "561.05 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "490.02 ms", - "Error": "9.651 ms", - "StdDev": "11.852 ms", - "Median": "489.68 ms" + "Mean": "507.18 ms", + "Error": "9.696 ms", + "StdDev": "12.262 ms", + "Median": "505.65 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "586.26 ms", - "Error": "9.405 ms", - "StdDev": "7.853 ms", - "Median": "585.77 ms" + "Mean": "655.45 ms", + "Error": "13.022 ms", + "StdDev": "27.750 ms", + "Median": "655.21 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "13.98 ms", - "Error": "0.276 ms", - "StdDev": "0.551 ms", - "Median": "13.82 ms" + "Version": "1.65.68", + "Mean": "16.57 ms", + "Error": "0.396 ms", + "StdDev": "1.104 ms", + "Median": "16.26 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "16.70 ms", - "Error": "0.316 ms", - "StdDev": "0.338 ms", - "Median": "16.80 ms" + "Mean": "20.29 ms", + "Error": "0.602 ms", + "StdDev": "1.727 ms", + "Median": "20.48 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/MassiveParallelTests.json b/docs/static/benchmarks/MassiveParallelTests.json index 063f3077c1f..7eaa679baa9 100644 --- a/docs/static/benchmarks/MassiveParallelTests.json +++ b/docs/static/benchmarks/MassiveParallelTests.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-08-23T00:20:42.432Z", + "timestamp": "2026-08-30T00:32:59.982Z", "category": "MassiveParallelTests", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", @@ -9,51 +9,51 @@ "results": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "471.8 ms", - "Error": "9.42 ms", - "StdDev": "18.81 ms", - "Median": "461.6 ms" + "Version": "1.65.68", + "Mean": "535.8 ms", + "Error": "10.30 ms", + "StdDev": "23.04 ms", + "Median": "538.1 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "1,083.0 ms", - "Error": "14.03 ms", - "StdDev": "13.12 ms", - "Median": "1,079.8 ms" + "Mean": "1,317.5 ms", + "Error": "26.01 ms", + "StdDev": "34.72 ms", + "Median": "1,310.7 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "2,975.1 ms", - "Error": "17.54 ms", - "StdDev": "14.65 ms", - "Median": "2,974.6 ms" + "Mean": "3,040.5 ms", + "Error": "43.17 ms", + "StdDev": "40.38 ms", + "Median": "3,027.4 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "1,289.6 ms", - "Error": "24.76 ms", - "StdDev": "23.16 ms", - "Median": "1,278.2 ms" + "Mean": "1,337.9 ms", + "Error": "23.87 ms", + "StdDev": "45.41 ms", + "Median": "1,332.9 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "218.2 ms", - "Error": "1.22 ms", - "StdDev": "1.08 ms", - "Median": "218.1 ms" + "Version": "1.65.68", + "Mean": "220.9 ms", + "Error": "0.69 ms", + "StdDev": "0.61 ms", + "Median": "221.1 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "673.0 ms", - "Error": "2.22 ms", - "StdDev": "1.97 ms", - "Median": "672.6 ms" + "Mean": "676.7 ms", + "Error": "1.78 ms", + "StdDev": "1.49 ms", + "Median": "677.0 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/MatrixTests.json b/docs/static/benchmarks/MatrixTests.json index f27c60d288a..0d44b971b1b 100644 --- a/docs/static/benchmarks/MatrixTests.json +++ b/docs/static/benchmarks/MatrixTests.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-08-23T00:20:42.432Z", + "timestamp": "2026-08-30T00:32:59.982Z", "category": "MatrixTests", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", @@ -9,51 +9,51 @@ "results": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "377.3 ms", - "Error": "7.30 ms", - "StdDev": "13.53 ms", - "Median": "374.3 ms" + "Version": "1.65.68", + "Mean": "364.9 ms", + "Error": "2.17 ms", + "StdDev": "1.92 ms", + "Median": "365.0 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "1,443.4 ms", - "Error": "15.85 ms", - "StdDev": "13.23 ms", - "Median": "1,438.9 ms" + "Mean": "1,536.5 ms", + "Error": "7.19 ms", + "StdDev": "6.01 ms", + "Median": "1,538.5 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "1,532.2 ms", - "Error": "27.62 ms", - "StdDev": "24.49 ms", - "Median": "1,528.3 ms" + "Mean": "1,497.2 ms", + "Error": "9.04 ms", + "StdDev": "8.02 ms", + "Median": "1,495.6 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "960.5 ms", - "Error": "19.04 ms", - "StdDev": "52.75 ms", - "Median": "957.4 ms" + "Mean": "861.9 ms", + "Error": "10.19 ms", + "StdDev": "9.53 ms", + "Median": "862.6 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "120.4 ms", - "Error": "1.26 ms", - "StdDev": "1.11 ms", - "Median": "120.5 ms" + "Version": "1.65.68", + "Mean": "117.1 ms", + "Error": "1.28 ms", + "StdDev": "1.20 ms", + "Median": "117.1 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "275.0 ms", - "Error": "1.16 ms", - "StdDev": "1.08 ms", - "Median": "275.1 ms" + "Mean": "269.2 ms", + "Error": "0.94 ms", + "StdDev": "0.88 ms", + "Median": "269.0 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/ScaleTests.json b/docs/static/benchmarks/ScaleTests.json index 92555d1396d..c944338adc3 100644 --- a/docs/static/benchmarks/ScaleTests.json +++ b/docs/static/benchmarks/ScaleTests.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-08-23T00:20:42.433Z", + "timestamp": "2026-08-30T00:32:59.982Z", "category": "ScaleTests", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", @@ -9,51 +9,51 @@ "results": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "280.02 ms", - "Error": "3.702 ms", - "StdDev": "3.282 ms", - "Median": "279.97 ms" + "Version": "1.65.68", + "Mean": "334.54 ms", + "Error": "8.520 ms", + "StdDev": "24.989 ms", + "Median": "334.15 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "522.04 ms", - "Error": "10.347 ms", - "StdDev": "22.927 ms", - "Median": "515.34 ms" + "Mean": "643.81 ms", + "Error": "12.855 ms", + "StdDev": "32.013 ms", + "Median": "636.45 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "505.40 ms", - "Error": "9.061 ms", - "StdDev": "12.702 ms", - "Median": "504.41 ms" + "Mean": "562.39 ms", + "Error": "11.182 ms", + "StdDev": "32.264 ms", + "Median": "560.91 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "620.62 ms", - "Error": "11.471 ms", - "StdDev": "15.701 ms", - "Median": "618.35 ms" + "Mean": "710.91 ms", + "Error": "14.031 ms", + "StdDev": "30.502 ms", + "Median": "708.11 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "19.86 ms", - "Error": "0.717 ms", - "StdDev": "2.114 ms", - "Median": "20.26 ms" + "Version": "1.65.68", + "Mean": "18.88 ms", + "Error": "0.353 ms", + "StdDev": "0.550 ms", + "Median": "18.84 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "23.07 ms", - "Error": "0.460 ms", - "StdDev": "0.874 ms", - "Median": "22.85 ms" + "Mean": "23.38 ms", + "Error": "0.463 ms", + "StdDev": "1.044 ms", + "Median": "23.59 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/SetupTeardownTests.json b/docs/static/benchmarks/SetupTeardownTests.json index 9dbeb85b016..14fc55c14bb 100644 --- a/docs/static/benchmarks/SetupTeardownTests.json +++ b/docs/static/benchmarks/SetupTeardownTests.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-08-23T00:20:42.433Z", + "timestamp": "2026-08-30T00:32:59.983Z", "category": "SetupTeardownTests", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", @@ -9,51 +9,51 @@ "results": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "389.40 ms", - "Error": "12.833 ms", - "StdDev": "37.638 ms", - "Median": "380.76 ms" + "Version": "1.65.68", + "Mean": "367.81 ms", + "Error": "7.319 ms", + "StdDev": "14.446 ms", + "Median": "364.64 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "1,090.23 ms", - "Error": "21.784 ms", - "StdDev": "46.892 ms", - "Median": "1,076.47 ms" + "Mean": "1,270.05 ms", + "Error": "25.203 ms", + "StdDev": "52.049 ms", + "Median": "1,267.62 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "1,163.10 ms", - "Error": "23.243 ms", - "StdDev": "60.412 ms", - "Median": "1,135.41 ms" + "Mean": "1,322.57 ms", + "Error": "24.873 ms", + "StdDev": "23.266 ms", + "Median": "1,319.06 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "784.00 ms", - "Error": "14.945 ms", - "StdDev": "20.951 ms", - "Median": "785.63 ms" + "Mean": "955.39 ms", + "Error": "18.634 ms", + "StdDev": "26.122 ms", + "Median": "959.77 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "70.18 ms", - "Error": "1.382 ms", - "StdDev": "2.527 ms", - "Median": "69.89 ms" + "Version": "1.65.68", + "Mean": "75.64 ms", + "Error": "1.441 ms", + "StdDev": "1.348 ms", + "Median": "75.74 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "179.90 ms", - "Error": "3.473 ms", - "StdDev": "3.249 ms", - "Median": "179.56 ms" + "Mean": "182.31 ms", + "Error": "1.800 ms", + "StdDev": "1.503 ms", + "Median": "182.43 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/historical.json b/docs/static/benchmarks/historical.json index be237104fae..41c0cb81c77 100644 --- a/docs/static/benchmarks/historical.json +++ b/docs/static/benchmarks/historical.json @@ -1,8 +1,4 @@ [ - { - "date": "2026-03-23", - "environment": "Ubuntu" - }, { "date": "2026-03-24", "environment": "Ubuntu" @@ -358,5 +354,9 @@ { "date": "2026-08-23", "environment": "Ubuntu" + }, + { + "date": "2026-08-30", + "environment": "Ubuntu" } ] \ No newline at end of file diff --git a/docs/static/benchmarks/latest.json b/docs/static/benchmarks/latest.json index 66781d049a5..588c42400d1 100644 --- a/docs/static/benchmarks/latest.json +++ b/docs/static/benchmarks/latest.json @@ -1,5 +1,5 @@ { - "timestamp": "2026-08-23T00:20:42.434Z", + "timestamp": "2026-08-30T00:32:59.983Z", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", "sdk": ".NET SDK 10.0.400", @@ -9,301 +9,301 @@ "AsyncTests": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "358.9 ms", - "Error": "3.57 ms", - "StdDev": "2.79 ms", - "Median": "358.6 ms" + "Version": "1.65.68", + "Mean": "388.5 ms", + "Error": "7.96 ms", + "StdDev": "22.97 ms", + "Median": "386.4 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "577.1 ms", - "Error": "9.43 ms", - "StdDev": "9.26 ms", - "Median": "574.3 ms" + "Mean": "714.3 ms", + "Error": "12.96 ms", + "StdDev": "19.79 ms", + "Median": "707.6 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "678.0 ms", - "Error": "13.43 ms", - "StdDev": "20.11 ms", - "Median": "673.8 ms" + "Mean": "664.8 ms", + "Error": "6.87 ms", + "StdDev": "5.74 ms", + "Median": "664.0 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "737.0 ms", - "Error": "13.67 ms", - "StdDev": "27.93 ms", + "Mean": "730.9 ms", + "Error": "14.38 ms", + "StdDev": "16.56 ms", "Median": "733.0 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "118.5 ms", - "Error": "1.19 ms", - "StdDev": "1.06 ms", - "Median": "118.6 ms" + "Version": "1.65.68", + "Mean": "116.0 ms", + "Error": "0.31 ms", + "StdDev": "0.29 ms", + "Median": "116.0 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "120.1 ms", - "Error": "1.32 ms", - "StdDev": "1.23 ms", - "Median": "120.0 ms" + "Mean": "118.5 ms", + "Error": "0.81 ms", + "StdDev": "0.68 ms", + "Median": "118.5 ms" } ], "DataDrivenTests": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "268.40 ms", - "Error": "2.883 ms", - "StdDev": "2.408 ms", - "Median": "267.52 ms" + "Version": "1.65.68", + "Mean": "281.32 ms", + "Error": "5.561 ms", + "StdDev": "11.233 ms", + "Median": "278.96 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "498.74 ms", - "Error": "9.322 ms", - "StdDev": "9.573 ms", - "Median": "496.12 ms" + "Mean": "564.15 ms", + "Error": "10.186 ms", + "StdDev": "16.735 ms", + "Median": "561.05 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "490.02 ms", - "Error": "9.651 ms", - "StdDev": "11.852 ms", - "Median": "489.68 ms" + "Mean": "507.18 ms", + "Error": "9.696 ms", + "StdDev": "12.262 ms", + "Median": "505.65 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "586.26 ms", - "Error": "9.405 ms", - "StdDev": "7.853 ms", - "Median": "585.77 ms" + "Mean": "655.45 ms", + "Error": "13.022 ms", + "StdDev": "27.750 ms", + "Median": "655.21 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "13.98 ms", - "Error": "0.276 ms", - "StdDev": "0.551 ms", - "Median": "13.82 ms" + "Version": "1.65.68", + "Mean": "16.57 ms", + "Error": "0.396 ms", + "StdDev": "1.104 ms", + "Median": "16.26 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "16.70 ms", - "Error": "0.316 ms", - "StdDev": "0.338 ms", - "Median": "16.80 ms" + "Mean": "20.29 ms", + "Error": "0.602 ms", + "StdDev": "1.727 ms", + "Median": "20.48 ms" } ], "MassiveParallelTests": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "471.8 ms", - "Error": "9.42 ms", - "StdDev": "18.81 ms", - "Median": "461.6 ms" + "Version": "1.65.68", + "Mean": "535.8 ms", + "Error": "10.30 ms", + "StdDev": "23.04 ms", + "Median": "538.1 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "1,083.0 ms", - "Error": "14.03 ms", - "StdDev": "13.12 ms", - "Median": "1,079.8 ms" + "Mean": "1,317.5 ms", + "Error": "26.01 ms", + "StdDev": "34.72 ms", + "Median": "1,310.7 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "2,975.1 ms", - "Error": "17.54 ms", - "StdDev": "14.65 ms", - "Median": "2,974.6 ms" + "Mean": "3,040.5 ms", + "Error": "43.17 ms", + "StdDev": "40.38 ms", + "Median": "3,027.4 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "1,289.6 ms", - "Error": "24.76 ms", - "StdDev": "23.16 ms", - "Median": "1,278.2 ms" + "Mean": "1,337.9 ms", + "Error": "23.87 ms", + "StdDev": "45.41 ms", + "Median": "1,332.9 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "218.2 ms", - "Error": "1.22 ms", - "StdDev": "1.08 ms", - "Median": "218.1 ms" + "Version": "1.65.68", + "Mean": "220.9 ms", + "Error": "0.69 ms", + "StdDev": "0.61 ms", + "Median": "221.1 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "673.0 ms", - "Error": "2.22 ms", - "StdDev": "1.97 ms", - "Median": "672.6 ms" + "Mean": "676.7 ms", + "Error": "1.78 ms", + "StdDev": "1.49 ms", + "Median": "677.0 ms" } ], "MatrixTests": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "377.3 ms", - "Error": "7.30 ms", - "StdDev": "13.53 ms", - "Median": "374.3 ms" + "Version": "1.65.68", + "Mean": "364.9 ms", + "Error": "2.17 ms", + "StdDev": "1.92 ms", + "Median": "365.0 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "1,443.4 ms", - "Error": "15.85 ms", - "StdDev": "13.23 ms", - "Median": "1,438.9 ms" + "Mean": "1,536.5 ms", + "Error": "7.19 ms", + "StdDev": "6.01 ms", + "Median": "1,538.5 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "1,532.2 ms", - "Error": "27.62 ms", - "StdDev": "24.49 ms", - "Median": "1,528.3 ms" + "Mean": "1,497.2 ms", + "Error": "9.04 ms", + "StdDev": "8.02 ms", + "Median": "1,495.6 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "960.5 ms", - "Error": "19.04 ms", - "StdDev": "52.75 ms", - "Median": "957.4 ms" + "Mean": "861.9 ms", + "Error": "10.19 ms", + "StdDev": "9.53 ms", + "Median": "862.6 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "120.4 ms", - "Error": "1.26 ms", - "StdDev": "1.11 ms", - "Median": "120.5 ms" + "Version": "1.65.68", + "Mean": "117.1 ms", + "Error": "1.28 ms", + "StdDev": "1.20 ms", + "Median": "117.1 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "275.0 ms", - "Error": "1.16 ms", - "StdDev": "1.08 ms", - "Median": "275.1 ms" + "Mean": "269.2 ms", + "Error": "0.94 ms", + "StdDev": "0.88 ms", + "Median": "269.0 ms" } ], "ScaleTests": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "280.02 ms", - "Error": "3.702 ms", - "StdDev": "3.282 ms", - "Median": "279.97 ms" + "Version": "1.65.68", + "Mean": "334.54 ms", + "Error": "8.520 ms", + "StdDev": "24.989 ms", + "Median": "334.15 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "522.04 ms", - "Error": "10.347 ms", - "StdDev": "22.927 ms", - "Median": "515.34 ms" + "Mean": "643.81 ms", + "Error": "12.855 ms", + "StdDev": "32.013 ms", + "Median": "636.45 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "505.40 ms", - "Error": "9.061 ms", - "StdDev": "12.702 ms", - "Median": "504.41 ms" + "Mean": "562.39 ms", + "Error": "11.182 ms", + "StdDev": "32.264 ms", + "Median": "560.91 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "620.62 ms", - "Error": "11.471 ms", - "StdDev": "15.701 ms", - "Median": "618.35 ms" + "Mean": "710.91 ms", + "Error": "14.031 ms", + "StdDev": "30.502 ms", + "Median": "708.11 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "19.86 ms", - "Error": "0.717 ms", - "StdDev": "2.114 ms", - "Median": "20.26 ms" + "Version": "1.65.68", + "Mean": "18.88 ms", + "Error": "0.353 ms", + "StdDev": "0.550 ms", + "Median": "18.84 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "23.07 ms", - "Error": "0.460 ms", - "StdDev": "0.874 ms", - "Median": "22.85 ms" + "Mean": "23.38 ms", + "Error": "0.463 ms", + "StdDev": "1.044 ms", + "Median": "23.59 ms" } ], "SetupTeardownTests": [ { "Method": "TUnit", - "Version": "1.65.38", - "Mean": "389.40 ms", - "Error": "12.833 ms", - "StdDev": "37.638 ms", - "Median": "380.76 ms" + "Version": "1.65.68", + "Mean": "367.81 ms", + "Error": "7.319 ms", + "StdDev": "14.446 ms", + "Median": "364.64 ms" }, { "Method": "NUnit", "Version": "4.6.1", - "Mean": "1,090.23 ms", - "Error": "21.784 ms", - "StdDev": "46.892 ms", - "Median": "1,076.47 ms" + "Mean": "1,270.05 ms", + "Error": "25.203 ms", + "StdDev": "52.049 ms", + "Median": "1,267.62 ms" }, { "Method": "MSTest", "Version": "4.3.3", - "Mean": "1,163.10 ms", - "Error": "23.243 ms", - "StdDev": "60.412 ms", - "Median": "1,135.41 ms" + "Mean": "1,322.57 ms", + "Error": "24.873 ms", + "StdDev": "23.266 ms", + "Median": "1,319.06 ms" }, { "Method": "xUnit3", "Version": "4.0.0", - "Mean": "784.00 ms", - "Error": "14.945 ms", - "StdDev": "20.951 ms", - "Median": "785.63 ms" + "Mean": "955.39 ms", + "Error": "18.634 ms", + "StdDev": "26.122 ms", + "Median": "959.77 ms" }, { "Method": "TUnit_AOT", - "Version": "1.65.38", - "Mean": "70.18 ms", - "Error": "1.382 ms", - "StdDev": "2.527 ms", - "Median": "69.89 ms" + "Version": "1.65.68", + "Mean": "75.64 ms", + "Error": "1.441 ms", + "StdDev": "1.348 ms", + "Median": "75.74 ms" }, { "Method": "xUnit3_AOT", "Version": "4.0.0", - "Mean": "179.90 ms", - "Error": "3.473 ms", - "StdDev": "3.249 ms", - "Median": "179.56 ms" + "Mean": "182.31 ms", + "Error": "1.800 ms", + "StdDev": "1.503 ms", + "Median": "182.43 ms" } ] }, @@ -311,35 +311,35 @@ "BuildTime": [ { "Method": "Build_TUnit", - "Version": "1.65.38", - "Mean": "917.6 ms", - "Error": "16.66 ms", - "StdDev": "26.90 ms", - "Median": "918.9 ms" + "Version": "1.65.68", + "Mean": "923.9 ms", + "Error": "18.16 ms", + "StdDev": "34.98 ms", + "Median": "915.4 ms" }, { "Method": "Build_NUnit", "Version": "4.6.1", - "Mean": "893.9 ms", - "Error": "13.98 ms", - "StdDev": "12.39 ms", - "Median": "899.1 ms" + "Mean": "884.6 ms", + "Error": "11.22 ms", + "StdDev": "9.95 ms", + "Median": "885.1 ms" }, { "Method": "Build_MSTest", "Version": "4.3.3", - "Mean": "1,036.7 ms", - "Error": "14.72 ms", - "StdDev": "13.77 ms", - "Median": "1,037.3 ms" + "Mean": "1,026.0 ms", + "Error": "20.50 ms", + "StdDev": "47.11 ms", + "Median": "1,019.4 ms" }, { "Method": "Build_xUnit3", "Version": "4.0.0", - "Mean": "862.9 ms", - "Error": "14.21 ms", - "StdDev": "12.60 ms", - "Median": "861.3 ms" + "Mean": "879.1 ms", + "Error": "9.05 ms", + "StdDev": "8.46 ms", + "Median": "881.5 ms" } ] }, @@ -347,6 +347,6 @@ "runtimeCategories": 6, "buildCategories": 1, "totalBenchmarks": 7, - "lastUpdated": "2026-08-23T00:20:42.431Z" + "lastUpdated": "2026-08-30T00:32:59.980Z" } } \ No newline at end of file diff --git a/docs/static/benchmarks/mocks/Callback.json b/docs/static/benchmarks/mocks/Callback.json index 8200088089f..343ee18aa73 100644 --- a/docs/static/benchmarks/mocks/Callback.json +++ b/docs/static/benchmarks/mocks/Callback.json @@ -1,117 +1,117 @@ { - "timestamp": "2026-08-26T02:57:20.474Z", + "timestamp": "2026-09-04T02:33:16.366Z", "category": "Callback", "description": "Callback registration and execution", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", "sdk": ".NET SDK 10.0.400", - "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3" + "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4" }, "results": [ { "Method": "TUnit.Mocks", - "Mean": "524.5 ns", - "Error": "4.74 ns", - "StdDev": "3.96 ns", + "Mean": "525.9 ns", + "Error": "10.29 ns", + "StdDev": "10.10 ns", "Gen0": "0.1898", "Gen1": "0.0019", "Allocated": "3.11 KB" }, { "Method": "Imposter", - "Mean": "377.2 ns", - "Error": "4.79 ns", - "StdDev": "4.48 ns", + "Mean": "356.3 ns", + "Error": "2.99 ns", + "StdDev": "2.79 ns", "Gen0": "0.1626", "Gen1": "0.0014", "Allocated": "2.66 KB" }, { "Method": "Mockolate", - "Mean": "274.2 ns", - "Error": "3.07 ns", - "StdDev": "2.87 ns", + "Mean": "277.9 ns", + "Error": "3.23 ns", + "StdDev": "3.02 ns", "Gen0": "0.1097", "Gen1": "0.0005", "Allocated": "1.8 KB" }, { "Method": "Moq", - "Mean": "108,008.1 ns", - "Error": "672.13 ns", - "StdDev": "561.26 ns", + "Mean": "107,115.3 ns", + "Error": "524.39 ns", + "StdDev": "490.52 ns", "Gen0": "0.7324", "Gen1": "0.4883", "Allocated": "13.29 KB" }, { "Method": "NSubstitute", - "Mean": "3,564.6 ns", - "Error": "55.53 ns", - "StdDev": "51.95 ns", + "Mean": "3,576.5 ns", + "Error": "59.01 ns", + "StdDev": "52.32 ns", "Gen0": "0.4578", "Gen1": "-", "Allocated": "7.85 KB" }, { "Method": "FakeItEasy", - "Mean": "3,801.3 ns", - "Error": "47.95 ns", - "StdDev": "44.85 ns", - "Gen0": "0.4501", - "Gen1": "0.0153", + "Mean": "3,780.5 ns", + "Error": "28.35 ns", + "StdDev": "23.67 ns", + "Gen0": "0.4539", + "Gen1": "0.0076", "Allocated": "7.44 KB" }, { "Method": "'TUnit.Mocks (with args)'", - "Mean": "626.7 ns", - "Error": "7.50 ns", - "StdDev": "6.65 ns", + "Mean": "600.6 ns", + "Error": "3.98 ns", + "StdDev": "3.53 ns", "Gen0": "0.1955", "Gen1": "0.0019", "Allocated": "3.2 KB" }, { "Method": "'Imposter (with args)'", - "Mean": "433.0 ns", - "Error": "1.87 ns", - "StdDev": "1.66 ns", + "Mean": "434.1 ns", + "Error": "2.38 ns", + "StdDev": "2.11 ns", "Gen0": "0.1726", "Gen1": "0.0014", "Allocated": "2.82 KB" }, { "Method": "'Mockolate (with args)'", - "Mean": "312.6 ns", - "Error": "3.49 ns", - "StdDev": "3.27 ns", + "Mean": "305.6 ns", + "Error": "2.68 ns", + "StdDev": "2.51 ns", "Gen0": "0.1125", "Gen1": "0.0005", "Allocated": "1.84 KB" }, { "Method": "'Moq (with args)'", - "Mean": "114,029.5 ns", - "Error": "664.83 ns", - "StdDev": "589.36 ns", + "Mean": "114,961.5 ns", + "Error": "454.49 ns", + "StdDev": "402.89 ns", "Gen0": "0.7324", "Gen1": "0.4883", "Allocated": "13.76 KB" }, { "Method": "'NSubstitute (with args)'", - "Mean": "3,972.7 ns", - "Error": "74.00 ns", - "StdDev": "69.22 ns", - "Gen0": "0.5112", - "Gen1": "0.0076", + "Mean": "3,942.0 ns", + "Error": "37.84 ns", + "StdDev": "35.40 ns", + "Gen0": "0.4883", + "Gen1": "-", "Allocated": "8.41 KB" }, { "Method": "'FakeItEasy (with args)'", - "Mean": "4,608.1 ns", - "Error": "91.72 ns", - "StdDev": "94.19 ns", + "Mean": "4,552.1 ns", + "Error": "61.89 ns", + "StdDev": "54.86 ns", "Gen0": "0.5646", "Gen1": "0.0153", "Allocated": "9.26 KB" diff --git a/docs/static/benchmarks/mocks/CombinedWorkflow.json b/docs/static/benchmarks/mocks/CombinedWorkflow.json index 5e395c6b28a..26a3dac132a 100644 --- a/docs/static/benchmarks/mocks/CombinedWorkflow.json +++ b/docs/static/benchmarks/mocks/CombinedWorkflow.json @@ -1,66 +1,66 @@ { - "timestamp": "2026-08-26T02:57:20.474Z", + "timestamp": "2026-09-04T02:33:16.366Z", "category": "CombinedWorkflow", "description": "Full workflow: create → setup → invoke → verify", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", "sdk": ".NET SDK 10.0.400", - "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3" + "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4" }, "results": [ { "Method": "TUnit.Mocks", - "Mean": "2.005 μs", - "Error": "0.0324 μs", - "StdDev": "0.0287 μs", - "Gen0": "0.3777", - "Gen1": "0.0038", + "Mean": "1.890 μs", + "Error": "0.0200 μs", + "StdDev": "0.0187 μs", + "Gen0": "0.3796", + "Gen1": "0.0057", "Allocated": "6.23 KB" }, { "Method": "Imposter", - "Mean": "2.783 μs", - "Error": "0.0556 μs", - "StdDev": "0.1002 μs", + "Mean": "2.892 μs", + "Error": "0.0578 μs", + "StdDev": "0.0540 μs", "Gen0": "0.9613", "Gen1": "0.0458", "Allocated": "15.71 KB" }, { "Method": "Mockolate", - "Mean": "1.790 μs", - "Error": "0.0342 μs", - "StdDev": "0.0380 μs", + "Mean": "1.680 μs", + "Error": "0.0194 μs", + "StdDev": "0.0172 μs", "Gen0": "0.4501", "Gen1": "0.0076", "Allocated": "7.36 KB" }, { "Method": "Moq", - "Mean": "303.325 μs", - "Error": "3.7601 μs", - "StdDev": "3.3332 μs", + "Mean": "404.656 μs", + "Error": "2.0986 μs", + "StdDev": "1.8603 μs", "Gen0": "1.9531", "Gen1": "0.9766", - "Allocated": "36.3 KB" + "Allocated": "36.49 KB" }, { "Method": "NSubstitute", - "Mean": "18.320 μs", - "Error": "0.1262 μs", - "StdDev": "0.1180 μs", - "Gen0": "1.6174", - "Gen1": "0.0305", + "Mean": "19.260 μs", + "Error": "0.0823 μs", + "StdDev": "0.0770 μs", + "Gen0": "1.5869", + "Gen1": "-", "Allocated": "26.72 KB" }, { "Method": "FakeItEasy", - "Mean": "16.434 μs", - "Error": "0.2877 μs", - "StdDev": "0.2550 μs", - "Gen0": "1.4648", - "Gen1": "0.1221", - "Allocated": "25.52 KB" + "Mean": "19.347 μs", + "Error": "0.1678 μs", + "StdDev": "0.1488 μs", + "Gen0": "1.5564", + "Gen1": "0.0305", + "Allocated": "25.85 KB" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/mocks/Invocation.json b/docs/static/benchmarks/mocks/Invocation.json index 3b9e00f1736..54b8a0eb004 100644 --- a/docs/static/benchmarks/mocks/Invocation.json +++ b/docs/static/benchmarks/mocks/Invocation.json @@ -1,171 +1,171 @@ { - "timestamp": "2026-08-26T02:57:20.474Z", + "timestamp": "2026-09-04T02:33:16.366Z", "category": "Invocation", "description": "Calling methods on mock objects", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", "sdk": ".NET SDK 10.0.400", - "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3" + "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4" }, "results": [ { "Method": "TUnit.Mocks", - "Mean": "276.9 ns", - "Error": "69.53 ns", - "StdDev": "3.81 ns", + "Mean": "276.11 ns", + "Error": "61.98 ns", + "StdDev": "3.397 ns", "Gen0": "0.0067", "Gen1": "0.0062", "Allocated": "128 B" }, { "Method": "Imposter", - "Mean": "298.5 ns", - "Error": "69.70 ns", - "StdDev": "3.82 ns", + "Mean": "303.36 ns", + "Error": "87.10 ns", + "StdDev": "4.774 ns", "Gen0": "0.0100", "Gen1": "0.0095", "Allocated": "168 B" }, { "Method": "Mockolate", - "Mean": "111.1 ns", - "Error": "16.94 ns", - "StdDev": "0.93 ns", + "Mean": "120.36 ns", + "Error": "50.28 ns", + "StdDev": "2.756 ns", "Gen0": "0.0050", "Gen1": "0.0048", "Allocated": "84 B" }, { "Method": "Moq", - "Mean": "810.2 ns", - "Error": "395.18 ns", - "StdDev": "21.66 ns", + "Mean": "813.27 ns", + "Error": "76.74 ns", + "StdDev": "4.206 ns", "Gen0": "0.0219", "Gen1": "0.0210", "Allocated": "376 B" }, { "Method": "NSubstitute", - "Mean": "749.4 ns", - "Error": "613.64 ns", - "StdDev": "33.64 ns", + "Mean": "710.74 ns", + "Error": "172.33 ns", + "StdDev": "9.446 ns", "Gen0": "0.0172", "Gen1": "0.0162", "Allocated": "304 B" }, { "Method": "FakeItEasy", - "Mean": "1,833.6 ns", - "Error": "350.91 ns", - "StdDev": "19.23 ns", + "Mean": "1,738.63 ns", + "Error": "161.03 ns", + "StdDev": "8.826 ns", "Gen0": "0.0553", "Gen1": "0.0534", "Allocated": "944 B" }, { "Method": "'TUnit.Mocks (String)'", - "Mean": "166.9 ns", - "Error": "87.98 ns", - "StdDev": "4.82 ns", + "Mean": "167.11 ns", + "Error": "74.09 ns", + "StdDev": "4.061 ns", "Gen0": "0.0052", "Gen1": "0.0050", "Allocated": "96 B" }, { "Method": "'Imposter (String)'", - "Mean": "303.1 ns", - "Error": "55.19 ns", - "StdDev": "3.03 ns", + "Mean": "291.19 ns", + "Error": "92.03 ns", + "StdDev": "5.045 ns", "Gen0": "0.0100", "Gen1": "0.0095", "Allocated": "168 B" }, { "Method": "'Mockolate (String)'", - "Mean": "100.8 ns", - "Error": "71.99 ns", - "StdDev": "3.95 ns", + "Mean": "93.41 ns", + "Error": "22.35 ns", + "StdDev": "1.225 ns", "Gen0": "0.0036", "Gen1": "0.0035", "Allocated": "60 B" }, { "Method": "'Moq (String)'", - "Mean": "564.4 ns", - "Error": "298.13 ns", - "StdDev": "16.34 ns", + "Mean": "532.70 ns", + "Error": "81.24 ns", + "StdDev": "4.453 ns", "Gen0": "0.0172", "Gen1": "0.0162", "Allocated": "296 B" }, { "Method": "'NSubstitute (String)'", - "Mean": "656.3 ns", - "Error": "101.12 ns", - "StdDev": "5.54 ns", - "Gen0": "0.0181", - "Gen1": "0.0172", - "Allocated": "328 B" + "Mean": "602.82 ns", + "Error": "102.70 ns", + "StdDev": "5.629 ns", + "Gen0": "0.0153", + "Gen1": "0.0143", + "Allocated": "272 B" }, { "Method": "'FakeItEasy (String)'", - "Mean": "1,623.5 ns", - "Error": "304.96 ns", - "StdDev": "16.72 ns", + "Mean": "1,545.15 ns", + "Error": "591.79 ns", + "StdDev": "32.438 ns", "Gen0": "0.0458", "Gen1": "0.0439", "Allocated": "776 B" }, { "Method": "'TUnit.Mocks (100 calls)'", - "Mean": "27,342.5 ns", - "Error": "10,466.71 ns", - "StdDev": "573.72 ns", + "Mean": "27,240.28 ns", + "Error": "9,886.83 ns", + "StdDev": "541.931 ns", "Gen0": "0.6409", "Gen1": "0.6104", "Allocated": "12736 B" }, { "Method": "'Imposter (100 calls)'", - "Mean": "29,495.1 ns", - "Error": "10,705.06 ns", - "StdDev": "586.78 ns", + "Mean": "29,050.40 ns", + "Error": "6,147.02 ns", + "StdDev": "336.939 ns", "Gen0": "0.9766", "Gen1": "0.9155", "Allocated": "16800 B" }, { "Method": "'Mockolate (100 calls)'", - "Mean": "10,825.3 ns", - "Error": "2,802.84 ns", - "StdDev": "153.63 ns", + "Mean": "10,561.82 ns", + "Error": "4,525.93 ns", + "StdDev": "248.081 ns", "Gen0": "0.4883", "Gen1": "0.4730", "Allocated": "8400 B" }, { "Method": "'Moq (100 calls)'", - "Mean": "83,855.2 ns", - "Error": "24,978.27 ns", - "StdDev": "1,369.14 ns", + "Mean": "79,428.50 ns", + "Error": "6,454.62 ns", + "StdDev": "353.799 ns", "Gen0": "2.1973", "Gen1": "2.0752", "Allocated": "37600 B" }, { "Method": "'NSubstitute (100 calls)'", - "Mean": "81,404.3 ns", - "Error": "34,696.37 ns", - "StdDev": "1,901.82 ns", - "Gen0": "1.9531", - "Gen1": "1.8311", - "Allocated": "36448 B" + "Mean": "70,130.53 ns", + "Error": "9,730.73 ns", + "StdDev": "533.374 ns", + "Gen0": "1.7090", + "Gen1": "1.5869", + "Allocated": "30848 B" }, { "Method": "'FakeItEasy (100 calls)'", - "Mean": "182,322.3 ns", - "Error": "55,668.73 ns", - "StdDev": "3,051.39 ns", + "Mean": "173,430.43 ns", + "Error": "34,832.13 ns", + "StdDev": "1,909.267 ns", "Gen0": "5.6152", "Gen1": "5.3711", "Allocated": "94400 B" diff --git a/docs/static/benchmarks/mocks/MockCreation.json b/docs/static/benchmarks/mocks/MockCreation.json index 01c2f58ef23..74ba10700bd 100644 --- a/docs/static/benchmarks/mocks/MockCreation.json +++ b/docs/static/benchmarks/mocks/MockCreation.json @@ -1,18 +1,18 @@ { - "timestamp": "2026-08-26T02:57:20.474Z", + "timestamp": "2026-09-04T02:33:16.366Z", "category": "MockCreation", "description": "Mock instance creation performance", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", "sdk": ".NET SDK 10.0.400", - "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3" + "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4" }, "results": [ { "Method": "TUnit.Mocks", - "Mean": "15.770 ns", - "Error": "0.2187 ns", - "StdDev": "0.2045 ns", + "Mean": "23.40 ns", + "Error": "0.224 ns", + "StdDev": "0.199 ns", "Gen0": "0.0120", "Gen1": "-", "Gen2": "-", @@ -20,29 +20,29 @@ }, { "Method": "Imposter", - "Mean": "52.184 ns", - "Error": "0.4922 ns", - "StdDev": "0.4363 ns", - "Gen0": "0.0263", + "Mean": "79.96 ns", + "Error": "0.386 ns", + "StdDev": "0.342 ns", + "Gen0": "0.0262", "Gen1": "-", "Gen2": "-", "Allocated": "440 B" }, { "Method": "Mockolate", - "Mean": "9.503 ns", - "Error": "0.2236 ns", - "StdDev": "0.2486 ns", - "Gen0": "0.0096", + "Mean": "13.99 ns", + "Error": "0.094 ns", + "StdDev": "0.088 ns", + "Gen0": "0.0095", "Gen1": "-", "Gen2": "-", "Allocated": "160 B" }, { "Method": "Moq", - "Mean": "745.059 ns", - "Error": "10.3641 ns", - "StdDev": "9.6946 ns", + "Mean": "1,010.23 ns", + "Error": "15.006 ns", + "StdDev": "14.036 ns", "Gen0": "0.1221", "Gen1": "-", "Gen2": "-", @@ -50,29 +50,29 @@ }, { "Method": "NSubstitute", - "Mean": "937.076 ns", - "Error": "11.4854 ns", - "StdDev": "10.7434 ns", - "Gen0": "0.2985", - "Gen1": "0.0029", + "Mean": "1,452.00 ns", + "Error": "14.914 ns", + "StdDev": "13.950 ns", + "Gen0": "0.2975", + "Gen1": "0.0019", "Gen2": "-", "Allocated": "5000 B" }, { "Method": "FakeItEasy", - "Mean": "995.678 ns", - "Error": "13.9343 ns", - "StdDev": "13.0341 ns", - "Gen0": "0.1612", + "Mean": "1,445.48 ns", + "Error": "28.528 ns", + "StdDev": "51.442 ns", + "Gen0": "0.1602", "Gen1": "0.0038", "Gen2": "0.0019", - "Allocated": "2714 B" + "Allocated": "2715 B" }, { "Method": "'TUnit.Mocks (Repository)'", - "Mean": "16.442 ns", - "Error": "0.2587 ns", - "StdDev": "0.2420 ns", + "Mean": "23.46 ns", + "Error": "0.239 ns", + "StdDev": "0.223 ns", "Gen0": "0.0120", "Gen1": "-", "Gen2": "-", @@ -80,19 +80,19 @@ }, { "Method": "'Imposter (Repository)'", - "Mean": "81.616 ns", - "Error": "0.7949 ns", - "StdDev": "0.7046 ns", - "Gen0": "0.0416", + "Mean": "123.02 ns", + "Error": "0.805 ns", + "StdDev": "0.753 ns", + "Gen0": "0.0415", "Gen1": "-", "Gen2": "-", "Allocated": "696 B" }, { "Method": "'Mockolate (Repository)'", - "Mean": "9.639 ns", - "Error": "0.1504 ns", - "StdDev": "0.1407 ns", + "Mean": "14.15 ns", + "Error": "0.129 ns", + "StdDev": "0.115 ns", "Gen0": "0.0105", "Gen1": "-", "Gen2": "-", @@ -100,33 +100,33 @@ }, { "Method": "'Moq (Repository)'", - "Mean": "694.943 ns", - "Error": "13.4170 ns", - "StdDev": "13.7783 ns", - "Gen0": "0.1135", + "Mean": "959.91 ns", + "Error": "9.791 ns", + "StdDev": "9.159 ns", + "Gen0": "0.1125", "Gen1": "-", "Gen2": "-", "Allocated": "1912 B" }, { "Method": "'NSubstitute (Repository)'", - "Mean": "935.775 ns", - "Error": "7.6325 ns", - "StdDev": "7.1394 ns", - "Gen0": "0.2985", - "Gen1": "0.0029", + "Mean": "1,385.63 ns", + "Error": "27.592 ns", + "StdDev": "35.877 ns", + "Gen0": "0.2975", + "Gen1": "0.0019", "Gen2": "-", "Allocated": "5000 B" }, { "Method": "'FakeItEasy (Repository)'", - "Mean": "1,006.596 ns", - "Error": "7.6581 ns", - "StdDev": "6.3949 ns", - "Gen0": "0.1612", + "Mean": "1,287.78 ns", + "Error": "23.041 ns", + "StdDev": "20.425 ns", + "Gen0": "0.1602", "Gen1": "0.0038", "Gen2": "0.0019", - "Allocated": "2714 B" + "Allocated": "2715 B" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/mocks/Setup.json b/docs/static/benchmarks/mocks/Setup.json index b2d72ab8a44..2c7ac980fe2 100644 --- a/docs/static/benchmarks/mocks/Setup.json +++ b/docs/static/benchmarks/mocks/Setup.json @@ -1,120 +1,120 @@ { - "timestamp": "2026-08-26T02:57:20.474Z", + "timestamp": "2026-09-04T02:33:16.366Z", "category": "Setup", "description": "Mock behavior configuration (returns, matchers)", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", "sdk": ".NET SDK 10.0.400", - "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3" + "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4" }, "results": [ { "Method": "TUnit.Mocks", - "Mean": "551.7 ns", - "Error": "10.96 ns", - "StdDev": "21.89 ns", - "Gen0": "0.1431", - "Gen1": "0.0010", + "Mean": "421.5 ns", + "Error": "7.07 ns", + "StdDev": "5.90 ns", + "Gen0": "0.0286", + "Gen1": "-", "Allocated": "2.34 KB" }, { "Method": "Imposter", - "Mean": "851.7 ns", - "Error": "16.20 ns", - "StdDev": "24.25 ns", - "Gen0": "0.3738", - "Gen1": "0.0076", + "Mean": "665.9 ns", + "Error": "13.34 ns", + "StdDev": "25.05 ns", + "Gen0": "0.0744", + "Gen1": "0.0010", "Allocated": "6.12 KB" }, { "Method": "Mockolate", - "Mean": "331.7 ns", - "Error": "6.55 ns", - "StdDev": "10.19 ns", - "Gen0": "0.0863", + "Mean": "255.6 ns", + "Error": "4.02 ns", + "StdDev": "3.36 ns", + "Gen0": "0.0172", "Gen1": "-", "Allocated": "1.41 KB" }, { "Method": "Moq", - "Mean": "433,988.1 ns", - "Error": "3,886.68 ns", - "StdDev": "3,635.60 ns", - "Gen0": "0.9766", + "Mean": "159,727.2 ns", + "Error": "2,598.80 ns", + "StdDev": "2,552.37 ns", + "Gen0": "0.2441", "Gen1": "-", - "Allocated": "28.68 KB" + "Allocated": "28.61 KB" }, { "Method": "NSubstitute", - "Mean": "6,263.8 ns", - "Error": "76.62 ns", - "StdDev": "63.98 ns", - "Gen0": "0.5493", + "Mean": "4,769.0 ns", + "Error": "93.76 ns", + "StdDev": "87.71 ns", + "Gen0": "0.0916", "Gen1": "-", "Allocated": "9.01 KB" }, { "Method": "FakeItEasy", - "Mean": "8,319.2 ns", - "Error": "152.28 ns", - "StdDev": "142.44 ns", - "Gen0": "0.6256", - "Gen1": "0.0153", - "Allocated": "10.45 KB" + "Mean": "4,569.9 ns", + "Error": "88.62 ns", + "StdDev": "118.30 ns", + "Gen0": "0.1221", + "Gen1": "0.0076", + "Allocated": "10.44 KB" }, { "Method": "'TUnit.Mocks (Multiple)'", - "Mean": "774.0 ns", - "Error": "15.36 ns", - "StdDev": "21.53 ns", - "Gen0": "0.1926", - "Gen1": "0.0019", + "Mean": "674.2 ns", + "Error": "12.98 ns", + "StdDev": "16.41 ns", + "Gen0": "0.0381", + "Gen1": "-", "Allocated": "3.15 KB" }, { "Method": "'Imposter (Multiple)'", - "Mean": "1,444.0 ns", - "Error": "22.32 ns", - "StdDev": "18.63 ns", - "Gen0": "0.6485", - "Gen1": "0.0248", + "Mean": "1,087.0 ns", + "Error": "21.54 ns", + "StdDev": "28.75 ns", + "Gen0": "0.1297", + "Gen1": "0.0038", "Allocated": "10.59 KB" }, { "Method": "'Mockolate (Multiple)'", - "Mean": "548.7 ns", - "Error": "10.31 ns", - "StdDev": "9.64 ns", - "Gen0": "0.1431", - "Gen1": "0.0010", + "Mean": "440.9 ns", + "Error": "6.52 ns", + "StdDev": "5.78 ns", + "Gen0": "0.0286", + "Gen1": "-", "Allocated": "2.35 KB" }, { "Method": "'Moq (Multiple)'", - "Mean": "114,312.6 ns", - "Error": "812.13 ns", - "StdDev": "719.93 ns", - "Gen0": "0.9766", - "Gen1": "0.7324", - "Allocated": "16.53 KB" + "Mean": "42,070.2 ns", + "Error": "453.37 ns", + "StdDev": "378.58 ns", + "Gen0": "0.1221", + "Gen1": "-", + "Allocated": "16.52 KB" }, { "Method": "'NSubstitute (Multiple)'", - "Mean": "12,314.6 ns", - "Error": "76.22 ns", - "StdDev": "63.64 ns", - "Gen0": "1.2207", + "Mean": "8,155.3 ns", + "Error": "161.65 ns", + "StdDev": "315.28 ns", + "Gen0": "0.2441", "Gen1": "-", - "Allocated": "20.31 KB" + "Allocated": "20.66 KB" }, { "Method": "'FakeItEasy (Multiple)'", - "Mean": "7,900.2 ns", - "Error": "127.71 ns", - "StdDev": "113.21 ns", - "Gen0": "0.6714", - "Gen1": "0.0610", - "Allocated": "11.71 KB" + "Mean": "4,233.0 ns", + "Error": "83.31 ns", + "StdDev": "129.70 ns", + "Gen0": "0.1221", + "Gen1": "-", + "Allocated": "11.7 KB" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/mocks/Verification.json b/docs/static/benchmarks/mocks/Verification.json index d51eb9cc06e..d8aeba994a2 100644 --- a/docs/static/benchmarks/mocks/Verification.json +++ b/docs/static/benchmarks/mocks/Verification.json @@ -1,174 +1,174 @@ { - "timestamp": "2026-08-26T02:57:20.474Z", + "timestamp": "2026-09-04T02:33:16.366Z", "category": "Verification", "description": "Verifying mock method calls", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", "sdk": ".NET SDK 10.0.400", - "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3" + "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4" }, "results": [ { "Method": "TUnit.Mocks", - "Mean": "760.75 ns", - "Error": "4.092 ns", - "StdDev": "3.828 ns", - "Gen0": "0.1793", - "Gen1": "0.0010", + "Mean": "996.63 ns", + "Error": "11.722 ns", + "StdDev": "10.965 ns", + "Gen0": "0.1183", + "Gen1": "-", "Allocated": "3008 B" }, { "Method": "Imposter", - "Mean": "680.80 ns", - "Error": "5.407 ns", - "StdDev": "4.793 ns", - "Gen0": "0.2794", - "Gen1": "0.0038", + "Mean": "1,029.90 ns", + "Error": "14.239 ns", + "StdDev": "12.622 ns", + "Gen0": "0.1850", + "Gen1": "0.0019", "Allocated": "4688 B" }, { "Method": "Mockolate", - "Mean": "398.57 ns", - "Error": "0.992 ns", - "StdDev": "0.829 ns", - "Gen0": "0.1268", - "Gen1": "0.0005", + "Mean": "582.88 ns", + "Error": "7.604 ns", + "StdDev": "7.113 ns", + "Gen0": "0.0839", + "Gen1": "-", "Allocated": "2128 B" }, { "Method": "Moq", - "Mean": "240,480.10 ns", - "Error": "1,310.717 ns", - "StdDev": "1,161.917 ns", - "Gen0": "0.9766", - "Gen1": "0.4883", - "Allocated": "24324 B" + "Mean": "256,197.99 ns", + "Error": "1,808.975 ns", + "StdDev": "1,603.609 ns", + "Gen0": "0.4883", + "Gen1": "-", + "Allocated": "24306 B" }, { "Method": "NSubstitute", - "Mean": "6,464.94 ns", - "Error": "50.175 ns", - "StdDev": "41.898 ns", - "Gen0": "0.5798", + "Mean": "7,438.36 ns", + "Error": "48.396 ns", + "StdDev": "42.902 ns", + "Gen0": "0.3662", "Gen1": "-", "Allocated": "10064 B" }, { "Method": "FakeItEasy", - "Mean": "6,411.13 ns", - "Error": "29.251 ns", - "StdDev": "25.930 ns", - "Gen0": "0.6409", + "Mean": "7,377.24 ns", + "Error": "43.861 ns", + "StdDev": "38.882 ns", + "Gen0": "0.4272", "Gen1": "0.0153", - "Allocated": "10722 B" + "Allocated": "10731 B" }, { "Method": "'TUnit.Mocks (Never)'", - "Mean": "55.52 ns", - "Error": "0.206 ns", - "StdDev": "0.183 ns", - "Gen0": "0.0191", + "Mean": "71.58 ns", + "Error": "1.433 ns", + "StdDev": "1.962 ns", + "Gen0": "0.0126", "Gen1": "-", "Allocated": "320 B" }, { "Method": "'Imposter (Never)'", - "Mean": "335.09 ns", - "Error": "0.901 ns", - "StdDev": "0.753 ns", - "Gen0": "0.1431", - "Gen1": "0.0010", + "Mean": "471.49 ns", + "Error": "6.386 ns", + "StdDev": "5.974 ns", + "Gen0": "0.0954", + "Gen1": "-", "Allocated": "2400 B" }, { "Method": "'Mockolate (Never)'", - "Mean": "243.15 ns", - "Error": "0.491 ns", - "StdDev": "0.435 ns", - "Gen0": "0.0682", + "Mean": "316.54 ns", + "Error": "6.311 ns", + "StdDev": "8.638 ns", + "Gen0": "0.0453", "Gen1": "-", "Allocated": "1144 B" }, { "Method": "'Moq (Never)'", - "Mean": "61,824.78 ns", - "Error": "234.600 ns", - "StdDev": "195.902 ns", - "Gen0": "0.3662", - "Gen1": "0.2441", + "Mean": "67,662.70 ns", + "Error": "422.716 ns", + "StdDev": "374.727 ns", + "Gen0": "0.2441", + "Gen1": "0.1221", "Allocated": "6925 B" }, { "Method": "'NSubstitute (Never)'", - "Mean": "3,588.04 ns", - "Error": "13.947 ns", - "StdDev": "12.363 ns", - "Gen0": "0.4234", - "Gen1": "0.0038", + "Mean": "3,982.31 ns", + "Error": "27.547 ns", + "StdDev": "25.767 ns", + "Gen0": "0.2823", + "Gen1": "-", "Allocated": "7088 B" }, { "Method": "'FakeItEasy (Never)'", - "Mean": "3,258.96 ns", - "Error": "49.439 ns", - "StdDev": "46.246 ns", - "Gen0": "0.3052", - "Gen1": "0.0153", - "Allocated": "5210 B" + "Mean": "3,817.50 ns", + "Error": "34.006 ns", + "StdDev": "30.145 ns", + "Gen0": "0.1831", + "Gen1": "-", + "Allocated": "5299 B" }, { "Method": "'TUnit.Mocks (Multiple)'", - "Mean": "1,261.68 ns", - "Error": "2.316 ns", - "StdDev": "2.167 ns", - "Gen0": "0.2670", + "Mean": "1,638.65 ns", + "Error": "14.438 ns", + "StdDev": "13.506 ns", + "Gen0": "0.1774", "Gen1": "0.0019", "Allocated": "4472 B" }, { "Method": "'Imposter (Multiple)'", - "Mean": "1,660.58 ns", - "Error": "5.565 ns", - "StdDev": "4.933 ns", - "Gen0": "0.6676", - "Gen1": "0.0210", + "Mean": "2,347.77 ns", + "Error": "46.412 ns", + "StdDev": "78.812 ns", + "Gen0": "0.4425", + "Gen1": "0.0114", "Allocated": "11192 B" }, { "Method": "'Mockolate (Multiple)'", - "Mean": "1,137.61 ns", - "Error": "3.246 ns", - "StdDev": "3.036 ns", - "Gen0": "0.3128", - "Gen1": "0.0038", + "Mean": "1,414.07 ns", + "Error": "24.072 ns", + "StdDev": "22.517 ns", + "Gen0": "0.2079", + "Gen1": "0.0019", "Allocated": "5240 B" }, { "Method": "'Moq (Multiple)'", - "Mean": "350,973.61 ns", - "Error": "2,881.199 ns", - "StdDev": "2,695.076 ns", - "Gen0": "1.9531", - "Gen1": "0.9766", - "Allocated": "34699 B" + "Mean": "356,518.29 ns", + "Error": "2,143.733 ns", + "StdDev": "1,900.364 ns", + "Gen0": "0.9766", + "Gen1": "-", + "Allocated": "34814 B" }, { "Method": "'NSubstitute (Multiple)'", - "Mean": "11,253.45 ns", - "Error": "35.831 ns", - "StdDev": "29.920 ns", - "Gen0": "0.9918", - "Gen1": "0.0153", + "Mean": "12,792.53 ns", + "Error": "64.138 ns", + "StdDev": "56.857 ns", + "Gen0": "0.6104", + "Gen1": "-", "Allocated": "16762 B" }, { "Method": "'FakeItEasy (Multiple)'", - "Mean": "11,742.28 ns", - "Error": "65.267 ns", - "StdDev": "61.051 ns", - "Gen0": "1.0986", - "Gen1": "0.0610", - "Allocated": "19344 B" + "Mean": "13,248.07 ns", + "Error": "33.295 ns", + "StdDev": "29.516 ns", + "Gen0": "0.7324", + "Gen1": "-", + "Allocated": "19238 B" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/mocks/latest.json b/docs/static/benchmarks/mocks/latest.json index 5914151bde9..c5bf20e0606 100644 --- a/docs/static/benchmarks/mocks/latest.json +++ b/docs/static/benchmarks/mocks/latest.json @@ -1,116 +1,116 @@ { - "timestamp": "2026-08-26T02:57:20.474Z", + "timestamp": "2026-09-04T02:33:16.366Z", "environment": { "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)", "sdk": ".NET SDK 10.0.400", - "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v3" + "host": ".NET 10.0.11 (10.0.11, 10.0.1126.37416), X64 RyuJIT x86-64-v4" }, "categories": { "Callback": [ { "Method": "TUnit.Mocks", - "Mean": "524.5 ns", - "Error": "4.74 ns", - "StdDev": "3.96 ns", + "Mean": "525.9 ns", + "Error": "10.29 ns", + "StdDev": "10.10 ns", "Gen0": "0.1898", "Gen1": "0.0019", "Allocated": "3.11 KB" }, { "Method": "Imposter", - "Mean": "377.2 ns", - "Error": "4.79 ns", - "StdDev": "4.48 ns", + "Mean": "356.3 ns", + "Error": "2.99 ns", + "StdDev": "2.79 ns", "Gen0": "0.1626", "Gen1": "0.0014", "Allocated": "2.66 KB" }, { "Method": "Mockolate", - "Mean": "274.2 ns", - "Error": "3.07 ns", - "StdDev": "2.87 ns", + "Mean": "277.9 ns", + "Error": "3.23 ns", + "StdDev": "3.02 ns", "Gen0": "0.1097", "Gen1": "0.0005", "Allocated": "1.8 KB" }, { "Method": "Moq", - "Mean": "108,008.1 ns", - "Error": "672.13 ns", - "StdDev": "561.26 ns", + "Mean": "107,115.3 ns", + "Error": "524.39 ns", + "StdDev": "490.52 ns", "Gen0": "0.7324", "Gen1": "0.4883", "Allocated": "13.29 KB" }, { "Method": "NSubstitute", - "Mean": "3,564.6 ns", - "Error": "55.53 ns", - "StdDev": "51.95 ns", + "Mean": "3,576.5 ns", + "Error": "59.01 ns", + "StdDev": "52.32 ns", "Gen0": "0.4578", "Gen1": "-", "Allocated": "7.85 KB" }, { "Method": "FakeItEasy", - "Mean": "3,801.3 ns", - "Error": "47.95 ns", - "StdDev": "44.85 ns", - "Gen0": "0.4501", - "Gen1": "0.0153", + "Mean": "3,780.5 ns", + "Error": "28.35 ns", + "StdDev": "23.67 ns", + "Gen0": "0.4539", + "Gen1": "0.0076", "Allocated": "7.44 KB" }, { "Method": "'TUnit.Mocks (with args)'", - "Mean": "626.7 ns", - "Error": "7.50 ns", - "StdDev": "6.65 ns", + "Mean": "600.6 ns", + "Error": "3.98 ns", + "StdDev": "3.53 ns", "Gen0": "0.1955", "Gen1": "0.0019", "Allocated": "3.2 KB" }, { "Method": "'Imposter (with args)'", - "Mean": "433.0 ns", - "Error": "1.87 ns", - "StdDev": "1.66 ns", + "Mean": "434.1 ns", + "Error": "2.38 ns", + "StdDev": "2.11 ns", "Gen0": "0.1726", "Gen1": "0.0014", "Allocated": "2.82 KB" }, { "Method": "'Mockolate (with args)'", - "Mean": "312.6 ns", - "Error": "3.49 ns", - "StdDev": "3.27 ns", + "Mean": "305.6 ns", + "Error": "2.68 ns", + "StdDev": "2.51 ns", "Gen0": "0.1125", "Gen1": "0.0005", "Allocated": "1.84 KB" }, { "Method": "'Moq (with args)'", - "Mean": "114,029.5 ns", - "Error": "664.83 ns", - "StdDev": "589.36 ns", + "Mean": "114,961.5 ns", + "Error": "454.49 ns", + "StdDev": "402.89 ns", "Gen0": "0.7324", "Gen1": "0.4883", "Allocated": "13.76 KB" }, { "Method": "'NSubstitute (with args)'", - "Mean": "3,972.7 ns", - "Error": "74.00 ns", - "StdDev": "69.22 ns", - "Gen0": "0.5112", - "Gen1": "0.0076", + "Mean": "3,942.0 ns", + "Error": "37.84 ns", + "StdDev": "35.40 ns", + "Gen0": "0.4883", + "Gen1": "-", "Allocated": "8.41 KB" }, { "Method": "'FakeItEasy (with args)'", - "Mean": "4,608.1 ns", - "Error": "91.72 ns", - "StdDev": "94.19 ns", + "Mean": "4,552.1 ns", + "Error": "61.89 ns", + "StdDev": "54.86 ns", "Gen0": "0.5646", "Gen1": "0.0153", "Allocated": "9.26 KB" @@ -119,218 +119,218 @@ "CombinedWorkflow": [ { "Method": "TUnit.Mocks", - "Mean": "2.005 μs", - "Error": "0.0324 μs", - "StdDev": "0.0287 μs", - "Gen0": "0.3777", - "Gen1": "0.0038", + "Mean": "1.890 μs", + "Error": "0.0200 μs", + "StdDev": "0.0187 μs", + "Gen0": "0.3796", + "Gen1": "0.0057", "Allocated": "6.23 KB" }, { "Method": "Imposter", - "Mean": "2.783 μs", - "Error": "0.0556 μs", - "StdDev": "0.1002 μs", + "Mean": "2.892 μs", + "Error": "0.0578 μs", + "StdDev": "0.0540 μs", "Gen0": "0.9613", "Gen1": "0.0458", "Allocated": "15.71 KB" }, { "Method": "Mockolate", - "Mean": "1.790 μs", - "Error": "0.0342 μs", - "StdDev": "0.0380 μs", + "Mean": "1.680 μs", + "Error": "0.0194 μs", + "StdDev": "0.0172 μs", "Gen0": "0.4501", "Gen1": "0.0076", "Allocated": "7.36 KB" }, { "Method": "Moq", - "Mean": "303.325 μs", - "Error": "3.7601 μs", - "StdDev": "3.3332 μs", + "Mean": "404.656 μs", + "Error": "2.0986 μs", + "StdDev": "1.8603 μs", "Gen0": "1.9531", "Gen1": "0.9766", - "Allocated": "36.3 KB" + "Allocated": "36.49 KB" }, { "Method": "NSubstitute", - "Mean": "18.320 μs", - "Error": "0.1262 μs", - "StdDev": "0.1180 μs", - "Gen0": "1.6174", - "Gen1": "0.0305", + "Mean": "19.260 μs", + "Error": "0.0823 μs", + "StdDev": "0.0770 μs", + "Gen0": "1.5869", + "Gen1": "-", "Allocated": "26.72 KB" }, { "Method": "FakeItEasy", - "Mean": "16.434 μs", - "Error": "0.2877 μs", - "StdDev": "0.2550 μs", - "Gen0": "1.4648", - "Gen1": "0.1221", - "Allocated": "25.52 KB" + "Mean": "19.347 μs", + "Error": "0.1678 μs", + "StdDev": "0.1488 μs", + "Gen0": "1.5564", + "Gen1": "0.0305", + "Allocated": "25.85 KB" } ], "Invocation": [ { "Method": "TUnit.Mocks", - "Mean": "276.9 ns", - "Error": "69.53 ns", - "StdDev": "3.81 ns", + "Mean": "276.11 ns", + "Error": "61.98 ns", + "StdDev": "3.397 ns", "Gen0": "0.0067", "Gen1": "0.0062", "Allocated": "128 B" }, { "Method": "Imposter", - "Mean": "298.5 ns", - "Error": "69.70 ns", - "StdDev": "3.82 ns", + "Mean": "303.36 ns", + "Error": "87.10 ns", + "StdDev": "4.774 ns", "Gen0": "0.0100", "Gen1": "0.0095", "Allocated": "168 B" }, { "Method": "Mockolate", - "Mean": "111.1 ns", - "Error": "16.94 ns", - "StdDev": "0.93 ns", + "Mean": "120.36 ns", + "Error": "50.28 ns", + "StdDev": "2.756 ns", "Gen0": "0.0050", "Gen1": "0.0048", "Allocated": "84 B" }, { "Method": "Moq", - "Mean": "810.2 ns", - "Error": "395.18 ns", - "StdDev": "21.66 ns", + "Mean": "813.27 ns", + "Error": "76.74 ns", + "StdDev": "4.206 ns", "Gen0": "0.0219", "Gen1": "0.0210", "Allocated": "376 B" }, { "Method": "NSubstitute", - "Mean": "749.4 ns", - "Error": "613.64 ns", - "StdDev": "33.64 ns", + "Mean": "710.74 ns", + "Error": "172.33 ns", + "StdDev": "9.446 ns", "Gen0": "0.0172", "Gen1": "0.0162", "Allocated": "304 B" }, { "Method": "FakeItEasy", - "Mean": "1,833.6 ns", - "Error": "350.91 ns", - "StdDev": "19.23 ns", + "Mean": "1,738.63 ns", + "Error": "161.03 ns", + "StdDev": "8.826 ns", "Gen0": "0.0553", "Gen1": "0.0534", "Allocated": "944 B" }, { "Method": "'TUnit.Mocks (String)'", - "Mean": "166.9 ns", - "Error": "87.98 ns", - "StdDev": "4.82 ns", + "Mean": "167.11 ns", + "Error": "74.09 ns", + "StdDev": "4.061 ns", "Gen0": "0.0052", "Gen1": "0.0050", "Allocated": "96 B" }, { "Method": "'Imposter (String)'", - "Mean": "303.1 ns", - "Error": "55.19 ns", - "StdDev": "3.03 ns", + "Mean": "291.19 ns", + "Error": "92.03 ns", + "StdDev": "5.045 ns", "Gen0": "0.0100", "Gen1": "0.0095", "Allocated": "168 B" }, { "Method": "'Mockolate (String)'", - "Mean": "100.8 ns", - "Error": "71.99 ns", - "StdDev": "3.95 ns", + "Mean": "93.41 ns", + "Error": "22.35 ns", + "StdDev": "1.225 ns", "Gen0": "0.0036", "Gen1": "0.0035", "Allocated": "60 B" }, { "Method": "'Moq (String)'", - "Mean": "564.4 ns", - "Error": "298.13 ns", - "StdDev": "16.34 ns", + "Mean": "532.70 ns", + "Error": "81.24 ns", + "StdDev": "4.453 ns", "Gen0": "0.0172", "Gen1": "0.0162", "Allocated": "296 B" }, { "Method": "'NSubstitute (String)'", - "Mean": "656.3 ns", - "Error": "101.12 ns", - "StdDev": "5.54 ns", - "Gen0": "0.0181", - "Gen1": "0.0172", - "Allocated": "328 B" + "Mean": "602.82 ns", + "Error": "102.70 ns", + "StdDev": "5.629 ns", + "Gen0": "0.0153", + "Gen1": "0.0143", + "Allocated": "272 B" }, { "Method": "'FakeItEasy (String)'", - "Mean": "1,623.5 ns", - "Error": "304.96 ns", - "StdDev": "16.72 ns", + "Mean": "1,545.15 ns", + "Error": "591.79 ns", + "StdDev": "32.438 ns", "Gen0": "0.0458", "Gen1": "0.0439", "Allocated": "776 B" }, { "Method": "'TUnit.Mocks (100 calls)'", - "Mean": "27,342.5 ns", - "Error": "10,466.71 ns", - "StdDev": "573.72 ns", + "Mean": "27,240.28 ns", + "Error": "9,886.83 ns", + "StdDev": "541.931 ns", "Gen0": "0.6409", "Gen1": "0.6104", "Allocated": "12736 B" }, { "Method": "'Imposter (100 calls)'", - "Mean": "29,495.1 ns", - "Error": "10,705.06 ns", - "StdDev": "586.78 ns", + "Mean": "29,050.40 ns", + "Error": "6,147.02 ns", + "StdDev": "336.939 ns", "Gen0": "0.9766", "Gen1": "0.9155", "Allocated": "16800 B" }, { "Method": "'Mockolate (100 calls)'", - "Mean": "10,825.3 ns", - "Error": "2,802.84 ns", - "StdDev": "153.63 ns", + "Mean": "10,561.82 ns", + "Error": "4,525.93 ns", + "StdDev": "248.081 ns", "Gen0": "0.4883", "Gen1": "0.4730", "Allocated": "8400 B" }, { "Method": "'Moq (100 calls)'", - "Mean": "83,855.2 ns", - "Error": "24,978.27 ns", - "StdDev": "1,369.14 ns", + "Mean": "79,428.50 ns", + "Error": "6,454.62 ns", + "StdDev": "353.799 ns", "Gen0": "2.1973", "Gen1": "2.0752", "Allocated": "37600 B" }, { "Method": "'NSubstitute (100 calls)'", - "Mean": "81,404.3 ns", - "Error": "34,696.37 ns", - "StdDev": "1,901.82 ns", - "Gen0": "1.9531", - "Gen1": "1.8311", - "Allocated": "36448 B" + "Mean": "70,130.53 ns", + "Error": "9,730.73 ns", + "StdDev": "533.374 ns", + "Gen0": "1.7090", + "Gen1": "1.5869", + "Allocated": "30848 B" }, { "Method": "'FakeItEasy (100 calls)'", - "Mean": "182,322.3 ns", - "Error": "55,668.73 ns", - "StdDev": "3,051.39 ns", + "Mean": "173,430.43 ns", + "Error": "34,832.13 ns", + "StdDev": "1,909.267 ns", "Gen0": "5.6152", "Gen1": "5.3711", "Allocated": "94400 B" @@ -339,9 +339,9 @@ "MockCreation": [ { "Method": "TUnit.Mocks", - "Mean": "15.770 ns", - "Error": "0.2187 ns", - "StdDev": "0.2045 ns", + "Mean": "23.40 ns", + "Error": "0.224 ns", + "StdDev": "0.199 ns", "Gen0": "0.0120", "Gen1": "-", "Gen2": "-", @@ -349,29 +349,29 @@ }, { "Method": "Imposter", - "Mean": "52.184 ns", - "Error": "0.4922 ns", - "StdDev": "0.4363 ns", - "Gen0": "0.0263", + "Mean": "79.96 ns", + "Error": "0.386 ns", + "StdDev": "0.342 ns", + "Gen0": "0.0262", "Gen1": "-", "Gen2": "-", "Allocated": "440 B" }, { "Method": "Mockolate", - "Mean": "9.503 ns", - "Error": "0.2236 ns", - "StdDev": "0.2486 ns", - "Gen0": "0.0096", + "Mean": "13.99 ns", + "Error": "0.094 ns", + "StdDev": "0.088 ns", + "Gen0": "0.0095", "Gen1": "-", "Gen2": "-", "Allocated": "160 B" }, { "Method": "Moq", - "Mean": "745.059 ns", - "Error": "10.3641 ns", - "StdDev": "9.6946 ns", + "Mean": "1,010.23 ns", + "Error": "15.006 ns", + "StdDev": "14.036 ns", "Gen0": "0.1221", "Gen1": "-", "Gen2": "-", @@ -379,29 +379,29 @@ }, { "Method": "NSubstitute", - "Mean": "937.076 ns", - "Error": "11.4854 ns", - "StdDev": "10.7434 ns", - "Gen0": "0.2985", - "Gen1": "0.0029", + "Mean": "1,452.00 ns", + "Error": "14.914 ns", + "StdDev": "13.950 ns", + "Gen0": "0.2975", + "Gen1": "0.0019", "Gen2": "-", "Allocated": "5000 B" }, { "Method": "FakeItEasy", - "Mean": "995.678 ns", - "Error": "13.9343 ns", - "StdDev": "13.0341 ns", - "Gen0": "0.1612", + "Mean": "1,445.48 ns", + "Error": "28.528 ns", + "StdDev": "51.442 ns", + "Gen0": "0.1602", "Gen1": "0.0038", "Gen2": "0.0019", - "Allocated": "2714 B" + "Allocated": "2715 B" }, { "Method": "'TUnit.Mocks (Repository)'", - "Mean": "16.442 ns", - "Error": "0.2587 ns", - "StdDev": "0.2420 ns", + "Mean": "23.46 ns", + "Error": "0.239 ns", + "StdDev": "0.223 ns", "Gen0": "0.0120", "Gen1": "-", "Gen2": "-", @@ -409,19 +409,19 @@ }, { "Method": "'Imposter (Repository)'", - "Mean": "81.616 ns", - "Error": "0.7949 ns", - "StdDev": "0.7046 ns", - "Gen0": "0.0416", + "Mean": "123.02 ns", + "Error": "0.805 ns", + "StdDev": "0.753 ns", + "Gen0": "0.0415", "Gen1": "-", "Gen2": "-", "Allocated": "696 B" }, { "Method": "'Mockolate (Repository)'", - "Mean": "9.639 ns", - "Error": "0.1504 ns", - "StdDev": "0.1407 ns", + "Mean": "14.15 ns", + "Error": "0.129 ns", + "StdDev": "0.115 ns", "Gen0": "0.0105", "Gen1": "-", "Gen2": "-", @@ -429,313 +429,313 @@ }, { "Method": "'Moq (Repository)'", - "Mean": "694.943 ns", - "Error": "13.4170 ns", - "StdDev": "13.7783 ns", - "Gen0": "0.1135", + "Mean": "959.91 ns", + "Error": "9.791 ns", + "StdDev": "9.159 ns", + "Gen0": "0.1125", "Gen1": "-", "Gen2": "-", "Allocated": "1912 B" }, { "Method": "'NSubstitute (Repository)'", - "Mean": "935.775 ns", - "Error": "7.6325 ns", - "StdDev": "7.1394 ns", - "Gen0": "0.2985", - "Gen1": "0.0029", + "Mean": "1,385.63 ns", + "Error": "27.592 ns", + "StdDev": "35.877 ns", + "Gen0": "0.2975", + "Gen1": "0.0019", "Gen2": "-", "Allocated": "5000 B" }, { "Method": "'FakeItEasy (Repository)'", - "Mean": "1,006.596 ns", - "Error": "7.6581 ns", - "StdDev": "6.3949 ns", - "Gen0": "0.1612", + "Mean": "1,287.78 ns", + "Error": "23.041 ns", + "StdDev": "20.425 ns", + "Gen0": "0.1602", "Gen1": "0.0038", "Gen2": "0.0019", - "Allocated": "2714 B" + "Allocated": "2715 B" } ], "Setup": [ { "Method": "TUnit.Mocks", - "Mean": "551.7 ns", - "Error": "10.96 ns", - "StdDev": "21.89 ns", - "Gen0": "0.1431", - "Gen1": "0.0010", + "Mean": "421.5 ns", + "Error": "7.07 ns", + "StdDev": "5.90 ns", + "Gen0": "0.0286", + "Gen1": "-", "Allocated": "2.34 KB" }, { "Method": "Imposter", - "Mean": "851.7 ns", - "Error": "16.20 ns", - "StdDev": "24.25 ns", - "Gen0": "0.3738", - "Gen1": "0.0076", + "Mean": "665.9 ns", + "Error": "13.34 ns", + "StdDev": "25.05 ns", + "Gen0": "0.0744", + "Gen1": "0.0010", "Allocated": "6.12 KB" }, { "Method": "Mockolate", - "Mean": "331.7 ns", - "Error": "6.55 ns", - "StdDev": "10.19 ns", - "Gen0": "0.0863", + "Mean": "255.6 ns", + "Error": "4.02 ns", + "StdDev": "3.36 ns", + "Gen0": "0.0172", "Gen1": "-", "Allocated": "1.41 KB" }, { "Method": "Moq", - "Mean": "433,988.1 ns", - "Error": "3,886.68 ns", - "StdDev": "3,635.60 ns", - "Gen0": "0.9766", + "Mean": "159,727.2 ns", + "Error": "2,598.80 ns", + "StdDev": "2,552.37 ns", + "Gen0": "0.2441", "Gen1": "-", - "Allocated": "28.68 KB" + "Allocated": "28.61 KB" }, { "Method": "NSubstitute", - "Mean": "6,263.8 ns", - "Error": "76.62 ns", - "StdDev": "63.98 ns", - "Gen0": "0.5493", + "Mean": "4,769.0 ns", + "Error": "93.76 ns", + "StdDev": "87.71 ns", + "Gen0": "0.0916", "Gen1": "-", "Allocated": "9.01 KB" }, { "Method": "FakeItEasy", - "Mean": "8,319.2 ns", - "Error": "152.28 ns", - "StdDev": "142.44 ns", - "Gen0": "0.6256", - "Gen1": "0.0153", - "Allocated": "10.45 KB" + "Mean": "4,569.9 ns", + "Error": "88.62 ns", + "StdDev": "118.30 ns", + "Gen0": "0.1221", + "Gen1": "0.0076", + "Allocated": "10.44 KB" }, { "Method": "'TUnit.Mocks (Multiple)'", - "Mean": "774.0 ns", - "Error": "15.36 ns", - "StdDev": "21.53 ns", - "Gen0": "0.1926", - "Gen1": "0.0019", + "Mean": "674.2 ns", + "Error": "12.98 ns", + "StdDev": "16.41 ns", + "Gen0": "0.0381", + "Gen1": "-", "Allocated": "3.15 KB" }, { "Method": "'Imposter (Multiple)'", - "Mean": "1,444.0 ns", - "Error": "22.32 ns", - "StdDev": "18.63 ns", - "Gen0": "0.6485", - "Gen1": "0.0248", + "Mean": "1,087.0 ns", + "Error": "21.54 ns", + "StdDev": "28.75 ns", + "Gen0": "0.1297", + "Gen1": "0.0038", "Allocated": "10.59 KB" }, { "Method": "'Mockolate (Multiple)'", - "Mean": "548.7 ns", - "Error": "10.31 ns", - "StdDev": "9.64 ns", - "Gen0": "0.1431", - "Gen1": "0.0010", + "Mean": "440.9 ns", + "Error": "6.52 ns", + "StdDev": "5.78 ns", + "Gen0": "0.0286", + "Gen1": "-", "Allocated": "2.35 KB" }, { "Method": "'Moq (Multiple)'", - "Mean": "114,312.6 ns", - "Error": "812.13 ns", - "StdDev": "719.93 ns", - "Gen0": "0.9766", - "Gen1": "0.7324", - "Allocated": "16.53 KB" + "Mean": "42,070.2 ns", + "Error": "453.37 ns", + "StdDev": "378.58 ns", + "Gen0": "0.1221", + "Gen1": "-", + "Allocated": "16.52 KB" }, { "Method": "'NSubstitute (Multiple)'", - "Mean": "12,314.6 ns", - "Error": "76.22 ns", - "StdDev": "63.64 ns", - "Gen0": "1.2207", + "Mean": "8,155.3 ns", + "Error": "161.65 ns", + "StdDev": "315.28 ns", + "Gen0": "0.2441", "Gen1": "-", - "Allocated": "20.31 KB" + "Allocated": "20.66 KB" }, { "Method": "'FakeItEasy (Multiple)'", - "Mean": "7,900.2 ns", - "Error": "127.71 ns", - "StdDev": "113.21 ns", - "Gen0": "0.6714", - "Gen1": "0.0610", - "Allocated": "11.71 KB" + "Mean": "4,233.0 ns", + "Error": "83.31 ns", + "StdDev": "129.70 ns", + "Gen0": "0.1221", + "Gen1": "-", + "Allocated": "11.7 KB" } ], "Verification": [ { "Method": "TUnit.Mocks", - "Mean": "760.75 ns", - "Error": "4.092 ns", - "StdDev": "3.828 ns", - "Gen0": "0.1793", - "Gen1": "0.0010", + "Mean": "996.63 ns", + "Error": "11.722 ns", + "StdDev": "10.965 ns", + "Gen0": "0.1183", + "Gen1": "-", "Allocated": "3008 B" }, { "Method": "Imposter", - "Mean": "680.80 ns", - "Error": "5.407 ns", - "StdDev": "4.793 ns", - "Gen0": "0.2794", - "Gen1": "0.0038", + "Mean": "1,029.90 ns", + "Error": "14.239 ns", + "StdDev": "12.622 ns", + "Gen0": "0.1850", + "Gen1": "0.0019", "Allocated": "4688 B" }, { "Method": "Mockolate", - "Mean": "398.57 ns", - "Error": "0.992 ns", - "StdDev": "0.829 ns", - "Gen0": "0.1268", - "Gen1": "0.0005", + "Mean": "582.88 ns", + "Error": "7.604 ns", + "StdDev": "7.113 ns", + "Gen0": "0.0839", + "Gen1": "-", "Allocated": "2128 B" }, { "Method": "Moq", - "Mean": "240,480.10 ns", - "Error": "1,310.717 ns", - "StdDev": "1,161.917 ns", - "Gen0": "0.9766", - "Gen1": "0.4883", - "Allocated": "24324 B" + "Mean": "256,197.99 ns", + "Error": "1,808.975 ns", + "StdDev": "1,603.609 ns", + "Gen0": "0.4883", + "Gen1": "-", + "Allocated": "24306 B" }, { "Method": "NSubstitute", - "Mean": "6,464.94 ns", - "Error": "50.175 ns", - "StdDev": "41.898 ns", - "Gen0": "0.5798", + "Mean": "7,438.36 ns", + "Error": "48.396 ns", + "StdDev": "42.902 ns", + "Gen0": "0.3662", "Gen1": "-", "Allocated": "10064 B" }, { "Method": "FakeItEasy", - "Mean": "6,411.13 ns", - "Error": "29.251 ns", - "StdDev": "25.930 ns", - "Gen0": "0.6409", + "Mean": "7,377.24 ns", + "Error": "43.861 ns", + "StdDev": "38.882 ns", + "Gen0": "0.4272", "Gen1": "0.0153", - "Allocated": "10722 B" + "Allocated": "10731 B" }, { "Method": "'TUnit.Mocks (Never)'", - "Mean": "55.52 ns", - "Error": "0.206 ns", - "StdDev": "0.183 ns", - "Gen0": "0.0191", + "Mean": "71.58 ns", + "Error": "1.433 ns", + "StdDev": "1.962 ns", + "Gen0": "0.0126", "Gen1": "-", "Allocated": "320 B" }, { "Method": "'Imposter (Never)'", - "Mean": "335.09 ns", - "Error": "0.901 ns", - "StdDev": "0.753 ns", - "Gen0": "0.1431", - "Gen1": "0.0010", + "Mean": "471.49 ns", + "Error": "6.386 ns", + "StdDev": "5.974 ns", + "Gen0": "0.0954", + "Gen1": "-", "Allocated": "2400 B" }, { "Method": "'Mockolate (Never)'", - "Mean": "243.15 ns", - "Error": "0.491 ns", - "StdDev": "0.435 ns", - "Gen0": "0.0682", + "Mean": "316.54 ns", + "Error": "6.311 ns", + "StdDev": "8.638 ns", + "Gen0": "0.0453", "Gen1": "-", "Allocated": "1144 B" }, { "Method": "'Moq (Never)'", - "Mean": "61,824.78 ns", - "Error": "234.600 ns", - "StdDev": "195.902 ns", - "Gen0": "0.3662", - "Gen1": "0.2441", + "Mean": "67,662.70 ns", + "Error": "422.716 ns", + "StdDev": "374.727 ns", + "Gen0": "0.2441", + "Gen1": "0.1221", "Allocated": "6925 B" }, { "Method": "'NSubstitute (Never)'", - "Mean": "3,588.04 ns", - "Error": "13.947 ns", - "StdDev": "12.363 ns", - "Gen0": "0.4234", - "Gen1": "0.0038", + "Mean": "3,982.31 ns", + "Error": "27.547 ns", + "StdDev": "25.767 ns", + "Gen0": "0.2823", + "Gen1": "-", "Allocated": "7088 B" }, { "Method": "'FakeItEasy (Never)'", - "Mean": "3,258.96 ns", - "Error": "49.439 ns", - "StdDev": "46.246 ns", - "Gen0": "0.3052", - "Gen1": "0.0153", - "Allocated": "5210 B" + "Mean": "3,817.50 ns", + "Error": "34.006 ns", + "StdDev": "30.145 ns", + "Gen0": "0.1831", + "Gen1": "-", + "Allocated": "5299 B" }, { "Method": "'TUnit.Mocks (Multiple)'", - "Mean": "1,261.68 ns", - "Error": "2.316 ns", - "StdDev": "2.167 ns", - "Gen0": "0.2670", + "Mean": "1,638.65 ns", + "Error": "14.438 ns", + "StdDev": "13.506 ns", + "Gen0": "0.1774", "Gen1": "0.0019", "Allocated": "4472 B" }, { "Method": "'Imposter (Multiple)'", - "Mean": "1,660.58 ns", - "Error": "5.565 ns", - "StdDev": "4.933 ns", - "Gen0": "0.6676", - "Gen1": "0.0210", + "Mean": "2,347.77 ns", + "Error": "46.412 ns", + "StdDev": "78.812 ns", + "Gen0": "0.4425", + "Gen1": "0.0114", "Allocated": "11192 B" }, { "Method": "'Mockolate (Multiple)'", - "Mean": "1,137.61 ns", - "Error": "3.246 ns", - "StdDev": "3.036 ns", - "Gen0": "0.3128", - "Gen1": "0.0038", + "Mean": "1,414.07 ns", + "Error": "24.072 ns", + "StdDev": "22.517 ns", + "Gen0": "0.2079", + "Gen1": "0.0019", "Allocated": "5240 B" }, { "Method": "'Moq (Multiple)'", - "Mean": "350,973.61 ns", - "Error": "2,881.199 ns", - "StdDev": "2,695.076 ns", - "Gen0": "1.9531", - "Gen1": "0.9766", - "Allocated": "34699 B" + "Mean": "356,518.29 ns", + "Error": "2,143.733 ns", + "StdDev": "1,900.364 ns", + "Gen0": "0.9766", + "Gen1": "-", + "Allocated": "34814 B" }, { "Method": "'NSubstitute (Multiple)'", - "Mean": "11,253.45 ns", - "Error": "35.831 ns", - "StdDev": "29.920 ns", - "Gen0": "0.9918", - "Gen1": "0.0153", + "Mean": "12,792.53 ns", + "Error": "64.138 ns", + "StdDev": "56.857 ns", + "Gen0": "0.6104", + "Gen1": "-", "Allocated": "16762 B" }, { "Method": "'FakeItEasy (Multiple)'", - "Mean": "11,742.28 ns", - "Error": "65.267 ns", - "StdDev": "61.051 ns", - "Gen0": "1.0986", - "Gen1": "0.0610", - "Allocated": "19344 B" + "Mean": "13,248.07 ns", + "Error": "33.295 ns", + "StdDev": "29.516 ns", + "Gen0": "0.7324", + "Gen1": "-", + "Allocated": "19238 B" } ] }, "stats": { "categoryCount": 6, "totalBenchmarks": 78, - "lastUpdated": "2026-08-26T02:57:20.474Z" + "lastUpdated": "2026-09-04T02:33:16.366Z" } } \ No newline at end of file diff --git a/docs/static/benchmarks/mocks/summary.json b/docs/static/benchmarks/mocks/summary.json index 18f560394a0..061203f9a9a 100644 --- a/docs/static/benchmarks/mocks/summary.json +++ b/docs/static/benchmarks/mocks/summary.json @@ -7,7 +7,7 @@ "Setup", "Verification" ], - "timestamp": "2026-08-26", + "timestamp": "2026-09-04", "environment": "Ubuntu Latest • .NET SDK 10.0.400", "libraries": [ "TUnit.Mocks", diff --git a/docs/static/benchmarks/summary.json b/docs/static/benchmarks/summary.json index 765b50488ff..4e143951a16 100644 --- a/docs/static/benchmarks/summary.json +++ b/docs/static/benchmarks/summary.json @@ -10,6 +10,6 @@ "build": [ "BuildTime" ], - "timestamp": "2026-08-23", + "timestamp": "2026-08-30", "environment": "Ubuntu Latest • .NET SDK 10.0.400" } \ No newline at end of file diff --git a/docs/yarn.lock b/docs/yarn.lock index d007c8385f4..83166275f8a 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -5385,9 +5385,9 @@ fast-json-stable-stringify@^2.0.0: integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-uri@^3.0.1: - version "3.1.5" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.5.tgz#610f37419a030270430cecd68d74e3d4d96725d0" - integrity sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw== + version "3.1.7" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.7.tgz#743157d957f3cbb4c65310e033dc2ad4ad7dc60a" + integrity sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg== fastq@^1.6.0: version "1.20.1" @@ -8576,10 +8576,10 @@ pvutils@^1.1.3, pvutils@^1.1.5: resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.2.0.tgz#4b5487a9cccd52d275b0538775790a3ca982bc22" integrity sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg== -qs@6.15.3, qs@~6.15.1: - version "6.15.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b" - integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A== +qs@6.16.0, qs@~6.15.1: + version "6.16.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.16.0.tgz#c22c723a28a920f3aacdce8289fabd43eccb79fd" + integrity sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA== dependencies: es-define-property "^1.0.1" side-channel "^1.1.1" @@ -9188,10 +9188,10 @@ send@~0.19.0, send@~0.19.1: range-parser "~1.2.1" statuses "~2.0.2" -serialize-javascript@7.1.0, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.1.0.tgz#9e462c5c6dec5dbc8b55d90c52a4ad6aff985b9f" - integrity sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw== +serialize-javascript@7.1.1, serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.1.1.tgz#c3b0a7ac13df6cf2fcda71cbb142ba9392a710b9" + integrity sha512-k3CMsaIvvdSwm8oLB4MXSl0wH2/cwlH7xGcnRd2DaeRmBkbzYmyT8j0tsX60DwD1eRwHTpNpH8ljKu9oUT1MeQ== serve-handler@^6.1.7: version "6.1.7" diff --git a/examples/CloudShop/CloudShop.Tests/CloudShop.Tests.csproj b/examples/CloudShop/CloudShop.Tests/CloudShop.Tests.csproj index 54b0ca24ca8..cb59a14be23 100644 --- a/examples/CloudShop/CloudShop.Tests/CloudShop.Tests.csproj +++ b/examples/CloudShop/CloudShop.Tests/CloudShop.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/scripts/Verify-DocSnippets.ps1 b/scripts/Verify-DocSnippets.ps1 index b755c886617..1c74e983140 100644 --- a/scripts/Verify-DocSnippets.ps1 +++ b/scripts/Verify-DocSnippets.ps1 @@ -17,6 +17,27 @@ $nugetConfigPath = Join-Path $generatedDirectory 'NuGet.config' $generatorProjectPath = Join-Path $repositoryRoot 'tools/TUnit.DocSnippetGenerator/TUnit.DocSnippetGenerator.csproj' $consumerProjectPath = Join-Path $repositoryRoot 'tests/TUnit.DocTests/TUnit.DocTests.csproj' +function Assert-StrictWarningPolicy([string]$projectPath) +{ + $propertyOutput = & dotnet msbuild $projectPath -nologo ` + '-getProperty:NoWarn;TreatWarningsAsErrors;WarningsNotAsErrors' + if ($LASTEXITCODE -ne 0) + { + throw "Could not evaluate warning policy for '$projectPath'." + } + + $properties = ($propertyOutput | Out-String | ConvertFrom-Json).Properties + if ($properties.TreatWarningsAsErrors -ne 'true' -or + $properties.NoWarn -or + $properties.WarningsNotAsErrors) + { + throw "Documentation builds require TreatWarningsAsErrors=true with empty NoWarn and WarningsNotAsErrors. Project: '$projectPath'." + } +} + +Assert-StrictWarningPolicy $generatorProjectPath +Assert-StrictWarningPolicy $consumerProjectPath + $requiredPackages = @( 'TUnit' 'TUnit.AspNetCore' @@ -95,3 +116,16 @@ if ($LASTEXITCODE -ne 0) { throw "Documentation snippet build failed with exit code $LASTEXITCODE." } + +$isolatedDirectory = Join-Path $generatedDirectory 'isolated' +foreach ($snippetDirectory in Get-ChildItem -LiteralPath $isolatedDirectory -Directory) +{ + & dotnet build $consumerProjectPath -c Release --no-restore --nologo '-clp:ErrorsOnly' ` + "-p:TUnitPackageVersion=$Version" ` + "-p:TUnitAssertionsShouldPackageVersion=$Version-beta" ` + "-p:GeneratedSnippetsDirectory=$($snippetDirectory.FullName)" + if ($LASTEXITCODE -ne 0) + { + throw "Documentation snippet build failed for $($snippetDirectory.Name) with exit code $LASTEXITCODE." + } +} diff --git a/src/TUnit.Assertions/Conditions/TypeAssertionExtensions.cs b/src/TUnit.Assertions/Conditions/TypeAssertionExtensions.cs index f81a4aadb04..2c9594a9a7b 100644 --- a/src/TUnit.Assertions/Conditions/TypeAssertionExtensions.cs +++ b/src/TUnit.Assertions/Conditions/TypeAssertionExtensions.cs @@ -1,4 +1,5 @@ using TUnit.Assertions.Attributes; +using TUnit.Assertions.Core; namespace TUnit.Assertions.Conditions; @@ -83,4 +84,21 @@ namespace TUnit.Assertions.Conditions; [AssertionFrom(nameof(Type.IsCOMObject), CustomName = "IsNotCOMObject", NegateLogic = true, ExpectationMessage = "be a COM object")] public static partial class TypeAssertionExtensions { + [GenerateAssertion(ExpectationMessage = "be assignable to {expectedType}", InlineMethodBody = true)] + public static AssertionResult IsAssignableTo(this Type value, Type expectedType) + => expectedType switch + { + null => AssertionResult.Failed("expected type was null"), + _ when expectedType.IsAssignableFrom(value) => AssertionResult.Passed, + _ => AssertionResult.Failed($"type {value.Name} is not assignable to {expectedType.Name}"), + }; + + [GenerateAssertion(ExpectationMessage = "be assignable from {sourceType}", InlineMethodBody = true)] + public static AssertionResult IsAssignableFrom(this Type value, Type sourceType) + => sourceType switch + { + null => AssertionResult.Failed("source type was null"), + _ when value.IsAssignableFrom(sourceType) => AssertionResult.Passed, + _ => AssertionResult.Failed($"type {value.Name} is not assignable from {sourceType.Name}"), + }; } diff --git a/src/TUnit.Assertions/Conditions/TypeOfAssertion.cs b/src/TUnit.Assertions/Conditions/TypeOfAssertion.cs index 31fba25b9e5..a644f935b35 100644 --- a/src/TUnit.Assertions/Conditions/TypeOfAssertion.cs +++ b/src/TUnit.Assertions/Conditions/TypeOfAssertion.cs @@ -147,18 +147,61 @@ protected override async Task CheckAsync(EvaluationMetadata $"to be assignable to {_targetType.Name}"; } +/// +/// Asserts that a represented is assignable to a target type while +/// retaining the represented type as the assertion value. +/// +public sealed class TypeIsAssignableToAssertion : Assertion +{ + private readonly Type _targetType = typeof(TTarget); + + public TypeIsAssignableToAssertion(AssertionContext context) + : base(context) + { + } + + protected override Task CheckAsync(EvaluationMetadata metadata) + { + if (metadata.Exception is { } exception) + { + return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}", exception)); + } + + if (metadata.Value is not { } representedType) + { + return Task.FromResult(AssertionResult.Failed("value was null")); + } + + return _targetType.IsAssignableFrom(representedType) + ? AssertionResult._passedTask + : Task.FromResult(AssertionResult.Failed( + $"type {representedType.Name} is not assignable to {_targetType.Name}")); + } + + protected override string GetExpectation() => $"to be assignable to {_targetType.Name}"; +} + /// /// Asserts that a value's type is NOT assignable to a specific type. /// Works with both direct value assertions and exception assertions (via .And after Throws). /// public class IsNotAssignableToAssertion : Assertion { + private readonly bool _useRepresentedType; private readonly Type _targetType; public IsNotAssignableToAssertion( AssertionContext context) + : this(context, useRepresentedType: false) + { + } + + internal IsNotAssignableToAssertion( + AssertionContext context, + bool useRepresentedType) : base(context) { + _useRepresentedType = useRepresentedType; _targetType = typeof(TTarget); } @@ -184,7 +227,9 @@ protected override Task CheckAsync(EvaluationMetadata m return Task.FromResult(AssertionResult.Failed("value was null")); } - var actualType = objectToCheck.GetType(); + var actualType = _useRepresentedType && objectToCheck is Type representedType + ? representedType + : objectToCheck.GetType(); if (!_targetType.IsAssignableFrom(actualType)) { @@ -204,12 +249,21 @@ protected override Task CheckAsync(EvaluationMetadata m /// public class IsAssignableFromAssertion : Assertion { + private readonly bool _useRepresentedType; private readonly Type _sourceType; public IsAssignableFromAssertion( AssertionContext context) + : this(context, useRepresentedType: false) + { + } + + internal IsAssignableFromAssertion( + AssertionContext context, + bool useRepresentedType) : base(context) { + _useRepresentedType = useRepresentedType; _sourceType = typeof(TSource); } @@ -233,7 +287,9 @@ protected override Task CheckAsync(EvaluationMetadata m return Task.FromResult(AssertionResult.Failed("value was null")); } - var actualType = objectToCheck.GetType(); + var actualType = _useRepresentedType && objectToCheck is Type representedType + ? representedType + : objectToCheck.GetType(); if (actualType.IsAssignableFrom(_sourceType)) { @@ -252,12 +308,21 @@ protected override Task CheckAsync(EvaluationMetadata m /// public class IsNotAssignableFromAssertion : Assertion { + private readonly bool _useRepresentedType; private readonly Type _sourceType; public IsNotAssignableFromAssertion( AssertionContext context) + : this(context, useRepresentedType: false) + { + } + + internal IsNotAssignableFromAssertion( + AssertionContext context, + bool useRepresentedType) : base(context) { + _useRepresentedType = useRepresentedType; _sourceType = typeof(TSource); } @@ -281,7 +346,9 @@ protected override Task CheckAsync(EvaluationMetadata m return Task.FromResult(AssertionResult.Failed("value was null")); } - var actualType = objectToCheck.GetType(); + var actualType = _useRepresentedType && objectToCheck is Type representedType + ? representedType + : objectToCheck.GetType(); if (!actualType.IsAssignableFrom(_sourceType)) { diff --git a/src/TUnit.Assertions/Extensions/Assert.cs b/src/TUnit.Assertions/Extensions/Assert.cs index 0c19e1bfa72..a4eeabe6156 100644 --- a/src/TUnit.Assertions/Extensions/Assert.cs +++ b/src/TUnit.Assertions/Extensions/Assert.cs @@ -254,6 +254,17 @@ public static HashSetAssertion That( return new CollectionAssertion(value.Cast(), expression); } + /// + /// Creates an assertion for a represented . + /// + [OverloadResolutionPriority(1)] + public static TypeValueAssertion That( + Type? value, + [CallerArgumentExpression(nameof(value))] string? expression = null) + { + return new TypeValueAssertion(value, expression); + } + /// /// Creates an assertion for an immediate value. /// Example: await Assert.That(42).IsEqualTo(42); diff --git a/src/TUnit.Assertions/Sources/TypeValueAssertion.cs b/src/TUnit.Assertions/Sources/TypeValueAssertion.cs new file mode 100644 index 00000000000..d6536a1908a --- /dev/null +++ b/src/TUnit.Assertions/Sources/TypeValueAssertion.cs @@ -0,0 +1,68 @@ +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Sources; + +/// +/// Source assertion for represented types. +/// +public sealed class TypeValueAssertion : ValueAssertion +{ + public TypeValueAssertion(Type? value, string? expression) + : base(value, expression) + { + } + + /// + /// Asserts that the represented type is assignable to . + /// The assertion retains the represented for awaiting and chaining. + /// + /// + /// Represented-type semantics apply only when this is the first assertion after Assert.That(type). + /// After .And or .Or, assignability assertions inspect the runtime type instead. + /// + public new TypeIsAssignableToAssertion IsAssignableTo() + { + Context.ExpressionBuilder.Append($".IsAssignableTo<{typeof(TTarget).Name}>()"); + return new TypeIsAssignableToAssertion(Context); + } + + /// + /// Asserts that the represented type is not assignable to . + /// + /// + /// Represented-type semantics apply only when this is the first assertion after Assert.That(type). + /// After .And or .Or, assignability assertions inspect the runtime type instead. + /// + public new IsNotAssignableToAssertion IsNotAssignableTo() + { + Context.ExpressionBuilder.Append($".IsNotAssignableTo<{typeof(TTarget).Name}>()"); + return new IsNotAssignableToAssertion(Context, useRepresentedType: true); + } + + /// + /// Asserts that is assignable to the represented type. + /// + /// + /// Represented-type semantics apply only when this is the first assertion after Assert.That(type). + /// After .And or .Or, assignability assertions inspect the runtime type instead. + /// + public new IsAssignableFromAssertion IsAssignableFrom() + { + Context.ExpressionBuilder.Append($".IsAssignableFrom<{typeof(TSource).Name}>()"); + return new IsAssignableFromAssertion(Context, useRepresentedType: true); + } + + /// + /// Asserts that is not assignable to the represented type. + /// + /// + /// Represented-type semantics apply only when this is the first assertion after Assert.That(type). + /// After .And or .Or, assignability assertions inspect the runtime type instead. + /// + public new IsNotAssignableFromAssertion IsNotAssignableFrom() + { + Context.ExpressionBuilder.Append($".IsNotAssignableFrom<{typeof(TSource).Name}>()"); + return new IsNotAssignableFromAssertion(Context, useRepresentedType: true); + } +} diff --git a/src/TUnit.Core/Settings/ReportingSettings.cs b/src/TUnit.Core/Settings/ReportingSettings.cs new file mode 100644 index 00000000000..a198a358b27 --- /dev/null +++ b/src/TUnit.Core/Settings/ReportingSettings.cs @@ -0,0 +1,28 @@ +namespace TUnit.Core.Settings; + +/// +/// Controls built-in report generation and publishing. +/// +public sealed class ReportingSettings +{ + internal ReportingSettings() { } + + /// + /// Whether to generate the HTML test report. Default: true. + /// Precedence: TUNIT_DISABLE_HTML_REPORTER → TUnitSettings → built-in default. + /// + public bool HtmlReportEnabled { get; set; } = true; + + /// + /// Whether to generate the machine-readable JSON report sidecar. Default: true. + /// Precedence: TUNIT_DISABLE_JSON_REPORT → TUnitSettings → built-in default. + /// + public bool JsonReportEnabled { get; set; } = true; + + /// + /// Whether to upload the HTML report as an artifact when supported by the CI environment. + /// Default: true. + /// Precedence: TUNIT_DISABLE_ARTIFACT_UPLOAD → TUnitSettings → built-in default. + /// + public bool ArtifactUploadEnabled { get; set; } = true; +} diff --git a/src/TUnit.Core/Settings/TUnitSettings.cs b/src/TUnit.Core/Settings/TUnitSettings.cs index cc15574a89b..7ca50b384a0 100644 --- a/src/TUnit.Core/Settings/TUnitSettings.cs +++ b/src/TUnit.Core/Settings/TUnitSettings.cs @@ -38,4 +38,9 @@ internal TUnitSettings() { } /// Controls test run behavior. /// public ExecutionSettings Execution { get; } = new(); + + /// + /// Controls report generation and publishing. + /// + public ReportingSettings Reporting { get; } = new(); } diff --git a/src/TUnit.Engine/Helpers/TimeoutDiagnostics.cs b/src/TUnit.Engine/Helpers/TimeoutDiagnostics.cs index c935abfb129..20c2cabb0b7 100644 --- a/src/TUnit.Engine/Helpers/TimeoutDiagnostics.cs +++ b/src/TUnit.Engine/Helpers/TimeoutDiagnostics.cs @@ -31,18 +31,22 @@ private static readonly (string Pattern, string Hint)[] DeadlockPatterns = /// /// The original timeout message. /// The task that was being executed when the timeout occurred. + /// An exception observed while the task handled timeout cancellation. /// An enhanced message with diagnostics appended. - public static string BuildTimeoutDiagnosticsMessage(string baseMessage, Task? executionTask) + public static string BuildTimeoutDiagnosticsMessage( + string baseMessage, + Task? executionTask, + Exception? executionException = null) { var sb = new StringBuilder(baseMessage); - AppendTaskStatus(sb, executionTask); + AppendTaskStatus(sb, executionTask, executionException); AppendStackTraceDiagnostics(sb); return sb.ToString(); } - private static void AppendTaskStatus(StringBuilder sb, Task? executionTask) + private static void AppendTaskStatus(StringBuilder sb, Task? executionTask, Exception? executionException) { if (executionTask is null) { @@ -55,22 +59,36 @@ private static void AppendTaskStatus(StringBuilder sb, Task? executionTask) sb.Append(executionTask.Status); sb.Append(" ---"); - if (executionTask.IsFaulted && executionTask.Exception is { } aggregateException) + if (executionException is not null) { - sb.AppendLine(); - sb.Append("Task exception: "); - + AppendTaskExceptionHeader(sb); + AppendTaskException(sb, executionException); + } + else if (executionTask.IsFaulted && executionTask.Exception is { } aggregateException) + { + AppendTaskExceptionHeader(sb); foreach (var innerException in aggregateException.InnerExceptions) { - sb.AppendLine(); - sb.Append(" "); - sb.Append(innerException.GetType().Name); - sb.Append(": "); - sb.Append(innerException.Message); + AppendTaskException(sb, innerException); } } } + private static void AppendTaskExceptionHeader(StringBuilder sb) + { + sb.AppendLine(); + sb.Append("Task exception: "); + } + + private static void AppendTaskException(StringBuilder sb, Exception exception) + { + sb.AppendLine(); + sb.Append(" "); + sb.Append(exception.GetType().Name); + sb.Append(": "); + sb.Append(exception.Message); + } + private static void AppendStackTraceDiagnostics(StringBuilder sb) { string stackTrace; diff --git a/src/TUnit.Engine/Helpers/TimeoutHelper.cs b/src/TUnit.Engine/Helpers/TimeoutHelper.cs index 901506d35c1..1b997f4b519 100644 --- a/src/TUnit.Engine/Helpers/TimeoutHelper.cs +++ b/src/TUnit.Engine/Helpers/TimeoutHelper.cs @@ -87,32 +87,87 @@ public static async Task ExecuteWithTimeoutAsync( } // Timeout occurred - give the execution task a brief grace period to clean up - try - { -#if NET8_0_OR_GREATER - await executionTask.WaitAsync(GracePeriod, CancellationToken.None).ConfigureAwait(false); -#else - // Use cancellable delay to avoid leaked tasks when executionTask completes first - using var graceCts = new CancellationTokenSource(); - var delayTask = Task.Delay(GracePeriod, graceCts.Token); - var graceWinner = await Task.WhenAny(executionTask, delayTask).ConfigureAwait(false); - if (graceWinner == executionTask) - { - graceCts.Cancel(); - } -#endif - } - catch - { - // Ignore all exceptions - task was cancelled, we're just giving it time to clean up - } + var executionException = await ObserveExceptionDuringGracePeriodAsync(executionTask).ConfigureAwait(false); + + // Routine cancellation adds no useful context; preserve exceptions explicitly + // thrown while handling cancellation, such as Aspire's diagnostic exception. + var exceptionToPreserve = IsRoutineCancellation(executionException, timeoutCts.Token) + ? null + : executionException; // Even if task completed during grace period, timeout already elapsed so we throw var baseMessage = timeoutMessage ?? $"Operation timed out after {timeout}"; - var diagnosticMessage = TimeoutDiagnostics.BuildTimeoutDiagnosticsMessage(baseMessage, executionTask); - throw new TimeoutException(diagnosticMessage); + var diagnosticMessage = TimeoutDiagnostics.BuildTimeoutDiagnosticsMessage(baseMessage, executionTask, exceptionToPreserve); + throw new TimeoutException(diagnosticMessage, exceptionToPreserve); } await executionTask.ConfigureAwait(false); } + + private static bool IsRoutineCancellation(Exception? exception, CancellationToken timeoutToken) + { + if (exception is not OperationCanceledException + { + InnerException: null + } operationCanceledException + || operationCanceledException.CancellationToken != timeoutToken) + { + return false; + } + + return operationCanceledException switch + { + TaskCanceledException taskCanceledException + when taskCanceledException.GetType() == typeof(TaskCanceledException) => + taskCanceledException.Message == new TaskCanceledException().Message, + { } when operationCanceledException.GetType() == typeof(OperationCanceledException) => + operationCanceledException.Message == new OperationCanceledException(timeoutToken).Message, + _ => false + }; + } + + private static async Task ObserveExceptionDuringGracePeriodAsync(Task executionTask) + { +#if NET8_0_OR_GREATER + try + { + await executionTask.WaitAsync(GracePeriod, CancellationToken.None).ConfigureAwait(false); + return null; + } + catch (TimeoutException) + { + return executionTask.IsCompleted + ? await ObserveCompletedTaskExceptionAsync(executionTask).ConfigureAwait(false) + : null; + } + catch (Exception ex) + { + return ex; + } +#else + // Use cancellable delay to avoid leaked tasks when executionTask completes first + using var graceCts = new CancellationTokenSource(); + var delayTask = Task.Delay(GracePeriod, graceCts.Token); + if (await Task.WhenAny(executionTask, delayTask).ConfigureAwait(false) != executionTask) + { + return null; + } + + graceCts.Cancel(); + return await ObserveCompletedTaskExceptionAsync(executionTask).ConfigureAwait(false); +#endif + } + + private static async Task ObserveCompletedTaskExceptionAsync(Task executionTask) + { + try + { + await executionTask.ConfigureAwait(false); + return null; + } + catch (Exception ex) + { + return ex; + } + } } diff --git a/src/TUnit.Engine/Reporters/Aggregation/ReportAggregator.cs b/src/TUnit.Engine/Reporters/Aggregation/ReportAggregator.cs index acba21e926d..72aac1d2ade 100644 --- a/src/TUnit.Engine/Reporters/Aggregation/ReportAggregator.cs +++ b/src/TUnit.Engine/Reporters/Aggregation/ReportAggregator.cs @@ -39,9 +39,9 @@ internal sealed class ReportAggregator private const string SidecarSearchPattern = "*" + ReportDataJson.SidecarExtension; private const string LockFileName = ".tunit-aggregate.lock"; - // Lock contention is expected (N processes finishing together, each holding the lock - // for a full merge), so wait far longer than the file-write retry defaults. - private const int LockMaxAttempts = 60; + // Bound lock contention to roughly ten seconds. Reporting must never hang test-suite + // completion; timed-out writers leave their sidecar for a later aggregate refresh. + private const int LockMaxAttempts = 30; private const int LockRetryDelayMs = 250; internal AggregationMode Mode { get; } @@ -130,12 +130,107 @@ internal string WriteSidecar(byte[] sidecarUtf8Json, string assemblyName, string { System.IO.Directory.CreateDirectory(Directory); - var fileName = $"{PathValidator.SanitizeFileName(assemblyName)}-{ShortHash(suiteSalt)}{ReportDataJson.SidecarExtension}"; - var path = Path.Combine(Directory, fileName); + var path = GetSidecarPath(assemblyName, suiteSalt); AtomicFile.WriteAllBytes(path, sidecarUtf8Json); return path; } + internal void WritePendingSidecar(byte[] sidecarUtf8Json, string assemblyName, string suiteSalt) + { + System.IO.Directory.CreateDirectory(Directory); + var path = GetPendingSidecarPath(assemblyName, suiteSalt); + AtomicFile.WriteAllBytes(path, sidecarUtf8Json); + } + + internal void DeletePendingSidecar(string assemblyName, string suiteSalt) + { + var path = GetPendingSidecarPath(assemblyName, suiteSalt); + File.Delete(path); + } + + internal string? ReadEffectiveSidecarGeneration(string assemblyName, string suiteSalt) + { + var pendingPath = GetPendingSidecarPath(assemblyName, suiteSalt); + var sidecarPath = File.Exists(pendingPath) + ? pendingPath + : GetSidecarPath(assemblyName, suiteSalt); + return File.Exists(sidecarPath) + ? ReportDataJson.GetPublicationGeneration(File.ReadAllBytes(sidecarPath)) + : null; + } + + internal void ExcludeSidecar(string assemblyName, string suiteSalt) + { + System.IO.Directory.CreateDirectory(Directory); + var currentGeneration = ReadEffectiveSidecarGeneration(assemblyName, suiteSalt); + AtomicFile.WriteAllText(GetExclusionMarkerPath(assemblyName, suiteSalt), currentGeneration ?? ""); + } + + internal void ExcludeSidecarIfGenerationMatches(string assemblyName, string suiteSalt, string? expectedGeneration) + { + System.IO.Directory.CreateDirectory(Directory); + var currentGeneration = ReadEffectiveSidecarGeneration(assemblyName, suiteSalt); + if (expectedGeneration is null + ? currentGeneration is not null + : !expectedGeneration.Equals(currentGeneration, StringComparison.Ordinal)) + { + return; + } + + AtomicFile.WriteAllText(GetExclusionMarkerPath(assemblyName, suiteSalt), currentGeneration ?? ""); + } + + internal void IncludeSidecar(string assemblyName, string suiteSalt) + { + File.Delete(GetExclusionMarkerPath(assemblyName, suiteSalt)); + } + + internal IDisposable BeginSidecarPublication(string assemblyName, string suiteSalt) + { + System.IO.Directory.CreateDirectory(Directory); + var lockPath = GetPublishingMarkerPath(assemblyName, suiteSalt); + return new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None); + } + + internal IDisposable? TryAcquireSidecarPublication(string assemblyName, string suiteSalt) + { + try + { + return BeginSidecarPublication(assemblyName, suiteSalt); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + return null; + } + } + + internal async Task AcquireSidecarPublicationAsync( + string assemblyName, + string suiteSalt, + CancellationToken cancellationToken) + { + System.IO.Directory.CreateDirectory(Directory); + return await AcquireFileLockAsync( + GetPublishingMarkerPath(assemblyName, suiteSalt), + cancellationToken, + "Warning: Report sidecar publication lock timed out; keeping the local per-suite report."); + } + + internal bool HasSidecarState(string assemblyName, string suiteSalt) + { + if (!System.IO.Directory.Exists(Directory)) + { + return false; + } + + var sidecarPath = GetSidecarPath(assemblyName, suiteSalt); + var pendingPath = GetPendingSidecarPath(assemblyName, suiteSalt); + return File.Exists(sidecarPath) + || File.Exists(pendingPath) + || File.Exists(sidecarPath + ReportDataJson.SidecarExclusionExtension) + || File.Exists(sidecarPath + ReportDataJson.SidecarPublishingExtension); + } + /// /// Reads every sidecar currently present in the shared directory. Unreadable or /// foreign files are skipped — a crashed sibling must not break the merge. @@ -154,12 +249,20 @@ internal List ReadAllSidecars() // as the tunit-report tool does. var seenDigests = new HashSet(StringComparer.Ordinal); using var sha = SHA256.Create(); - foreach (var file in System.IO.Directory.GetFiles(Directory, SidecarSearchPattern)) + var effectiveSidecars = ReportDataJson.SelectEffectiveSidecars( + System.IO.Directory.GetFiles(Directory, SidecarSearchPattern)); + foreach (var file in effectiveSidecars) { try { + if (ReportDataJson.IsSidecarPublicationInProgress(file)) + { + continue; + } + var bytes = File.ReadAllBytes(file); - if (seenDigests.Add(Convert.ToBase64String(sha.ComputeHash(bytes))) + if (!ReportDataJson.IsSidecarExcluded(file, bytes) + && seenDigests.Add(Convert.ToBase64String(sha.ComputeHash(bytes))) && ReportDataJson.TryDeserialize((ReadOnlyMemory)bytes) is { } data) { results.Add(data); @@ -178,14 +281,22 @@ internal List ReadAllSidecars() /// /// Acquires the cross-process aggregation lock. Every writer performs its whole /// read-merge-write cycle under this lock, so merges never interleave. Returns - /// when the lock cannot be acquired within the timeout; - /// callers should then skip merging (a later sibling will produce a fresher merge). + /// after a bounded wait so reporting cannot hang the run. /// internal async Task AcquireLockAsync(CancellationToken cancellationToken) { System.IO.Directory.CreateDirectory(Directory); - var lockPath = Path.Combine(Directory, LockFileName); + return await AcquireFileLockAsync( + Path.Combine(Directory, LockFileName), + cancellationToken, + "Warning: Report aggregation lock timed out; keeping per-suite reports and deferring aggregate refresh."); + } + private static async Task AcquireFileLockAsync( + string lockPath, + CancellationToken cancellationToken, + string timeoutWarning) + { for (var attempt = 1; attempt <= LockMaxAttempts; attempt++) { cancellationToken.ThrowIfCancellationRequested(); @@ -195,8 +306,6 @@ internal List ReadAllSidecars() } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { - // Contention (or a permissions hiccup) on the final attempt must still fall - // through to the graceful "skip this merge" path, never escape as a throw. if (attempt == LockMaxAttempts) { break; @@ -206,7 +315,7 @@ internal List ReadAllSidecars() } } - Console.WriteLine("Warning: Could not acquire the report aggregation lock; skipping merge for this process."); + Console.WriteLine(timeoutWarning); return null; } @@ -231,4 +340,23 @@ private static string ShortHash(string value) var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(value)); return BitConverter.ToString(hash, 0, 4).Replace("-", "").ToLowerInvariant(); } + + private string GetExclusionMarkerPath(string assemblyName, string suiteSalt) + { + return GetSidecarPath(assemblyName, suiteSalt) + ReportDataJson.SidecarExclusionExtension; + } + + private string GetPublishingMarkerPath(string assemblyName, string suiteSalt) + { + return GetSidecarPath(assemblyName, suiteSalt) + ReportDataJson.SidecarPublishingExtension; + } + + private string GetPendingSidecarPath(string assemblyName, string suiteSalt) + => ReportDataJson.GetPendingSidecarPath(GetSidecarPath(assemblyName, suiteSalt)); + + private string GetSidecarPath(string assemblyName, string suiteSalt) + { + var fileName = $"{PathValidator.SanitizeFileName(assemblyName)}-{ShortHash(suiteSalt)}{ReportDataJson.SidecarExtension}"; + return Path.Combine(Directory, fileName); + } } diff --git a/src/TUnit.Engine/Reporters/Aggregation/ReportDataJson.cs b/src/TUnit.Engine/Reporters/Aggregation/ReportDataJson.cs index ee2ebecf3dd..2088bb690b2 100644 --- a/src/TUnit.Engine/Reporters/Aggregation/ReportDataJson.cs +++ b/src/TUnit.Engine/Reporters/Aggregation/ReportDataJson.cs @@ -25,10 +25,108 @@ internal static class ReportDataJson /// File extension shared by every sidecar so aggregators can discover them. internal const string SidecarExtension = ".tunit-report.json"; + internal const string SidecarPendingSegment = ".pending"; + internal const string SidecarExclusionExtension = ".excluded"; + internal const string SidecarPublishingExtension = ".publishing"; /// Merged HTML report filename, shared by the engine and the tool's default output. internal const string MergedReportFileName = "merged-report.html"; + internal static bool IsSidecarExcluded(string sidecarPath, ReadOnlySpan sidecarUtf8Json) + { + var canonicalPath = GetCanonicalSidecarPath(sidecarPath); + var exclusionPath = canonicalPath + SidecarExclusionExtension; + if (!File.Exists(exclusionPath)) + { + return false; + } + + var generation = GetPublicationGeneration(sidecarUtf8Json); + if (generation is null) + { + // Exclusions created for sidecars from before generation tracking remain valid. + return true; + } + + try + { + return File.ReadAllText(exclusionPath).Equals(generation, StringComparison.Ordinal); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + return true; + } + } + + internal static bool IsSidecarPublicationInProgress(string sidecarPath) + { + // Pending sidecars are complete atomic files written only after a publisher's + // per-suite lock wait expires. The canonical publisher cannot mutate them. + if (IsPendingSidecar(sidecarPath)) + { + return false; + } + + var publicationLockPath = sidecarPath + SidecarPublishingExtension; + if (!File.Exists(publicationLockPath)) + { + return false; + } + + try + { + // Lock file is stable: deleting it after releasing the handle lets another + // process lock the old inode while a third process creates and locks a new one. + using (new FileStream(publicationLockPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + { + } + return false; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + return true; + } + } + + internal static string? GetPublicationGeneration(ReadOnlySpan sidecarUtf8Json) + { + try + { + var reader = new Utf8JsonReader(sidecarUtf8Json); + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName + && reader.ValueTextEquals("publicationGeneration") + && reader.Read() + && reader.TokenType == JsonTokenType.String) + { + return reader.GetString(); + } + } + } + catch (JsonException) + { + } + + return null; + } + + internal static bool IsPendingSidecar(string sidecarPath) + => sidecarPath.EndsWith(SidecarPendingSegment + SidecarExtension, StringComparison.Ordinal); + + internal static string GetCanonicalSidecarPath(string sidecarPath) + => IsPendingSidecar(sidecarPath) + ? sidecarPath[..^(SidecarPendingSegment.Length + SidecarExtension.Length)] + SidecarExtension + : sidecarPath; + + internal static string GetPendingSidecarPath(string canonicalSidecarPath) + => canonicalSidecarPath[..^SidecarExtension.Length] + SidecarPendingSegment + SidecarExtension; + + internal static IEnumerable SelectEffectiveSidecars(IEnumerable sidecarPaths) + => sidecarPaths + .GroupBy(GetCanonicalSidecarPath, StringComparer.Ordinal) + .Select(group => group.FirstOrDefault(IsPendingSidecar) ?? group.First()); + /// /// Serializes straight to UTF-8 bytes — callers write the same payload to more than one /// file, so producing bytes once avoids a UTF-8 → string → UTF-8 round trip per copy. @@ -50,6 +148,7 @@ private static void Write(Utf8JsonWriter w, ReportData data) { w.WriteStartObject(); w.WriteNumber("schemaVersion", SchemaVersion); + w.WriteString("publicationGeneration", data.PublicationGeneration ?? Guid.NewGuid().ToString("N")); w.WriteString("assemblyName", data.AssemblyName); w.WriteString("machineName", data.MachineName); w.WriteString("timestamp", data.Timestamp); @@ -322,6 +421,7 @@ private static bool IsMalformedSidecar(Exception ex) return new ReportData { AssemblyName = GetString(root, "assemblyName") ?? "Unknown", + PublicationGeneration = GetString(root, "publicationGeneration"), MachineName = GetString(root, "machineName") ?? "", Timestamp = GetString(root, "timestamp") ?? "", TUnitVersion = GetString(root, "tunitVersion") ?? "", diff --git a/src/TUnit.Engine/Reporters/GitHubReporter.cs b/src/TUnit.Engine/Reporters/GitHubReporter.cs index cf3c21d5b0b..9348d130211 100644 --- a/src/TUnit.Engine/Reporters/GitHubReporter.cs +++ b/src/TUnit.Engine/Reporters/GitHubReporter.cs @@ -470,6 +470,16 @@ public async Task AfterRunAsync(int exitCode, CancellationToken cancellation) /// internal bool SuppressPerSuiteSummary { get; set; } + internal void ResetSessionState() + { + _latestUpdates.Clear(); + _terminalStateCounts.Clear(); + ArtifactUrl = null; + ShowArtifactUploadTip = false; + SuppressPerSuiteSummary = false; + _runStopwatch = Stopwatch.StartNew(); + } + /// /// Renders the aggregated block and rewrites the marked summary region. Called by /// HtmlReporter inside the aggregation lock, so the whole merge is one lock cycle. @@ -494,6 +504,14 @@ internal void WriteAggregatedSummary(IReadOnlyList suites, stri GitHubSummaryRegion.ReplaceOrAppend(_outputSummaryFilePath, content, MaxFileSizeInBytes); } + internal void ClearAggregatedSummary() + { + if (_outputSummaryFilePath is not null) + { + GitHubSummaryRegion.ReplaceOrAppend(_outputSummaryFilePath, string.Empty, MaxFileSizeInBytes); + } + } + private async Task WriteFile(string contents) { var fileInfo = new FileInfo(_outputSummaryFilePath); diff --git a/src/TUnit.Engine/Reporters/Html/HtmlReportDataModel.cs b/src/TUnit.Engine/Reporters/Html/HtmlReportDataModel.cs index eac0d3b23a7..95ec9473143 100644 --- a/src/TUnit.Engine/Reporters/Html/HtmlReportDataModel.cs +++ b/src/TUnit.Engine/Reporters/Html/HtmlReportDataModel.cs @@ -12,6 +12,10 @@ internal sealed class ReportData [JsonPropertyName("assemblyName")] public required string AssemblyName { get; init; } + /// Atomic sidecar generation used only by aggregation cleanup. + [JsonIgnore] + public string? PublicationGeneration { get; init; } + [JsonPropertyName("machineName")] public required string MachineName { get; init; } diff --git a/src/TUnit.Engine/Reporters/Html/HtmlReporter.cs b/src/TUnit.Engine/Reporters/Html/HtmlReporter.cs index 4f3f4a51846..7ff1cf76618 100644 --- a/src/TUnit.Engine/Reporters/Html/HtmlReporter.cs +++ b/src/TUnit.Engine/Reporters/Html/HtmlReporter.cs @@ -12,6 +12,7 @@ using Microsoft.Testing.Platform.Services; using Microsoft.Testing.Platform.TestHost; using TUnit.Core; +using TUnit.Core.Settings; using TUnit.Engine.Configuration; using TUnit.Engine.Constants; using TUnit.Engine.Exceptions; @@ -27,6 +28,8 @@ namespace TUnit.Engine.Reporters.Html; internal sealed class HtmlReporter(IExtension extension) : IDataConsumer, IDataProducer, ITestHostApplicationLifetime, ITestSessionLifetimeHandler, IFilterReceiver, IDisposable { + private const int HtmlReportEnabledUnresolved = -1; + // System.Text.Json's Utf8JsonWriter limits a single string token to int.MaxValue / 6 characters. // Truncate large outputs early so report generation never fails for test suites with excessive logging. internal const int MaxOutputLength = 1 * 1024 * 1024; // 1 MB @@ -35,7 +38,9 @@ internal sealed class HtmlReporter(IExtension extension) : IDataConsumer, IDataP private IMessageBus? _messageBus; private string _resultsDirectory = "TestResults"; private readonly ConcurrentDictionary _updates = []; + private readonly object _htmlReportStateLock = new(); private GitHubReporter? _githubReporter; + private int _htmlReportEnabledAfterDiscovery = HtmlReportEnabledUnresolved; #if NET private ActivityCollector? _activityCollector; @@ -43,7 +48,7 @@ internal sealed class HtmlReporter(IExtension extension) : IDataConsumer, IDataP public async Task IsEnabledAsync() { - if (IsTruthyEnv(Environment.GetEnvironmentVariable(EnvironmentConstants.DisableHtmlReporter))) + if (!IsHtmlReportEnabled()) { return false; } @@ -61,6 +66,11 @@ public async Task IsEnabledAsync() public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) { + if (!IsHtmlReportEnabledForRun()) + { + return Task.CompletedTask; + } + var testNodeUpdateMessage = (TestNodeUpdateMessage)value; // Keep only the update we'll report per test: a final-state update always wins over a // non-final one, otherwise the latest wins. The engine emits a single final update per @@ -89,10 +99,6 @@ private static bool HasFinalState(TestNodeUpdateMessage update) public Task BeforeRunAsync(CancellationToken cancellationToken) { -#if NET - _activityCollector = new ActivityCollector(); - _activityCollector.Start(); -#endif return Task.CompletedTask; } @@ -100,21 +106,62 @@ public Task AfterRunAsync(int exitCode, CancellationToken cancellation) => Task.CompletedTask; // All work happens in OnTestSessionFinishingAsync. public Task OnTestSessionStartingAsync(ITestSessionContext testSessionContext) - => Task.CompletedTask; + { + lock (_htmlReportStateLock) + { + _updates.Clear(); + _githubReporter?.ResetSessionState(); + Volatile.Write(ref _htmlReportEnabledAfterDiscovery, HtmlReportEnabledUnresolved); +#if NET + DisposeActivityCollection(); + // Discovery hooks may re-enable reporting for this session. Start before + // discovery so those early spans are retained, then resolve the setting + // on the first post-discovery update. + StartActivityCollection(); +#endif + } + + return Task.CompletedTask; + } public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionContext) { try { #if NET - _activityCollector?.Stop(); + StopActivityCollection(); +#endif + + if (!IsHtmlReportEnabledForRun()) + { +#if NET + TraceRegistry.Clear(); #endif + _updates.Clear(); + var disabledOutputPath = _outputPath ?? GetDefaultOutputPath(); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable); + await DeleteSidecarsAndRefreshAggregateAsync( + GetAssemblyName(), + disabledOutputPath, + aggregator); + return; + } if (_updates.Count == 0) { #if NET TraceRegistry.Clear(); #endif + if (!IsJsonReportEnabled()) + { + var emptyOutputPath = _outputPath ?? GetDefaultOutputPath(); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable); + await DeleteSidecarsAndRefreshAggregateAsync( + GetAssemblyName(), + emptyOutputPath, + aggregator); + } + return; } @@ -163,26 +210,36 @@ public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionCon { Console.WriteLine($"Warning: HTML report generation failed: {ex.Message}"); } + finally + { +#if NET + DisposeActivityCollection(); +#endif + } } - private async Task TryWriteSidecarAndAggregateAsync(ReportData reportData, string htmlOutputPath, CancellationToken cancellationToken) + internal async Task TryWriteSidecarAndAggregateAsync(ReportData reportData, string htmlOutputPath, CancellationToken cancellationToken) { + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable); + + if (!IsJsonReportEnabled()) + { + await DeleteSidecarsAndRefreshAggregateAsync(reportData.AssemblyName, htmlOutputPath, aggregator); + return; + } + // Serialized once; the same bytes back both the local sidecar and the shared copy. var sidecarBytes = ReportDataJson.SerializeToBytes(reportData); - if (!IsTruthyEnv(Environment.GetEnvironmentVariable(EnvironmentConstants.DisableJsonReport))) + try { - try - { - AtomicFile.WriteAllBytes(GetSidecarPath(htmlOutputPath), sidecarBytes); - } - catch (Exception ex) - { - Console.WriteLine($"Warning: Failed to write JSON report sidecar: {ex.Message}"); - } + AtomicFile.WriteAllBytes(GetSidecarPath(htmlOutputPath), sidecarBytes); + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Failed to write JSON report sidecar: {ex.Message}"); } - var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable); if (aggregator is null) { return; @@ -190,44 +247,152 @@ private async Task TryWriteSidecarAndAggregateAsync(ReportData reportData, strin try { - aggregator.WriteSidecar(sidecarBytes, reportData.AssemblyName, suiteSalt: htmlOutputPath); + // Serialize the shared write with cleanup so cleanup cannot mistake an in-flight + // enabled generation for the stale generation it intended to remove. + using var publicationMarker = await aggregator.AcquireSidecarPublicationAsync( + reportData.AssemblyName, + htmlOutputPath, + CancellationToken.None); + if (publicationMarker is null) + { + // Publication-lock contention must not discard completed suite results. + // A separate pending slot cannot overwrite the active publisher's canonical + // generation and remains available to this or any later aggregate refresh. + aggregator.WritePendingSidecar(sidecarBytes, reportData.AssemblyName, suiteSalt: htmlOutputPath); + } + else + { + // Owning the lock makes it safe for a newer normal publication to replace + // any pending timeout result left by an earlier publisher. + aggregator.DeletePendingSidecar(reportData.AssemblyName, htmlOutputPath); + aggregator.WriteSidecar(sidecarBytes, reportData.AssemblyName, suiteSalt: htmlOutputPath); + aggregator.IncludeSidecar(reportData.AssemblyName, htmlOutputPath); + } - // Aggregation is committed for this suite: whatever happens below, the classic - // per-suite block must not be appended on top of the aggregated one. (If we - // fail past this point, a sibling that merges after us still renders this - // suite's results from the sidecar just written.) - if (_githubReporter is not null) + if (aggregator.Mode == AggregationMode.Defer && _githubReporter is not null) { _githubReporter.SuppressPerSuiteSummary = true; } - // Every finishing process regenerates the merged outputs from all sidecars - // present so far; the last one to finish leaves the complete aggregate. - using var aggregationLock = await aggregator.AcquireLockAsync(cancellationToken); + IDisposable? aggregationLock; + try + { + aggregationLock = await aggregator.AcquireLockAsync(cancellationToken); + } + catch (OperationCanceledException) + { + aggregationLock = null; + } + if (aggregationLock is null) { return; } - var suites = aggregator.ReadAllSidecars(); - if (suites.Count == 0) + using (aggregationLock) + { + publicationMarker?.Dispose(); + + RefreshAggregatedOutputs(aggregator); + if (_githubReporter is not null) + { + _githubReporter.SuppressPerSuiteSummary = true; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Warning: Report aggregation failed: {ex.Message}"); + } + } + + private async Task DeleteSidecarsAndRefreshAggregateAsync( + string assemblyName, + string htmlOutputPath, + ReportAggregator? aggregator) + { + TryDeleteReportFile(() => File.Delete(GetSidecarPath(htmlOutputPath))); + + if (aggregator is null) + { + return; + } + + if (!aggregator.HasSidecarState(assemblyName, htmlOutputPath)) + { + return; + } + + try + { + var expectedGeneration = aggregator.ReadEffectiveSidecarGeneration(assemblyName, htmlOutputPath); + using var publicationMarker = aggregator.TryAcquireSidecarPublication(assemblyName, htmlOutputPath); + if (publicationMarker is null) + { + // An enabled publication may already have installed its generation while + // holding this lock. Cleanup must not wait, acquire next, and hide it. + return; + } + + // Exclude only the generation observed before the lock attempt. A publisher that + // won the per-suite lock in the meantime installed a newer generation and survives. + aggregator.ExcludeSidecarIfGenerationMatches(assemblyName, htmlOutputPath, expectedGeneration); + + // The exclusion is durable and generation-scoped, so do not hold the per-suite + // lock during the bounded aggregate-lock wait. A staged enabled publication + // must be able to acquire it and clear the exclusion before either wait expires. + publicationMarker.Dispose(); + + // Cleanup must survive session cancellation or stale enabled-run sidecars can + // re-enter a sibling's aggregate. Lock acquisition remains time-bounded. + using var aggregationLock = await aggregator.AcquireLockAsync(CancellationToken.None); + if (aggregationLock is null) { return; } - aggregator.WriteMergedHtml(suites); - Console.WriteLine($"Merged HTML test report ({suites.Count} {(suites.Count == 1 ? "suite" : "suites")} so far) written to: {aggregator.MergedReportPath}"); + RefreshAggregatedOutputs(aggregator); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + Console.WriteLine($"Warning: Report aggregation cleanup failed: {ex.Message}"); + } + } - // The step summary is rewritten in the same lock cycle: one env parse, one - // lock acquisition and one sidecar scan per process for both merged outputs. + private void RefreshAggregatedOutputs(ReportAggregator aggregator) + { + var suites = aggregator.ReadAllSidecars(); + if (suites.Count == 0) + { + TryDeleteReportFile(() => File.Delete(aggregator.MergedReportPath)); if (aggregator.Mode == AggregationMode.Cooperative) { - _githubReporter?.WriteAggregatedSummary(suites, aggregator.MergedReportPath); + _githubReporter?.ClearAggregatedSummary(); } + + return; + } + + aggregator.WriteMergedHtml(suites); + Console.WriteLine($"Merged HTML test report ({suites.Count} {(suites.Count == 1 ? "suite" : "suites")} so far) written to: {aggregator.MergedReportPath}"); + + // The step summary is rewritten in the same lock cycle: one env parse, one + // lock acquisition and one sidecar scan per process for both merged outputs. + if (aggregator.Mode == AggregationMode.Cooperative) + { + _githubReporter?.WriteAggregatedSummary(suites, aggregator.MergedReportPath); + } + } + + private static void TryDeleteReportFile(Action deleteFile) + { + try + { + deleteFile(); } catch (Exception ex) { - Console.WriteLine($"Warning: Report aggregation failed: {ex.Message}"); + Console.WriteLine($"Warning: Failed to remove disabled report file: {ex.Message}"); } } @@ -238,6 +403,53 @@ private static bool IsTruthyEnv(string? value) value.Equals("1", StringComparison.Ordinal) || value.Equals("yes", StringComparison.OrdinalIgnoreCase)); + internal static bool IsHtmlReportEnabled() + => !IsTruthyEnv(Environment.GetEnvironmentVariable(EnvironmentConstants.DisableHtmlReporter)) + && TUnitSettings.Default.Reporting.HtmlReportEnabled; + + internal static bool IsJsonReportEnabled() + => !IsTruthyEnv(Environment.GetEnvironmentVariable(EnvironmentConstants.DisableJsonReport)) + && TUnitSettings.Default.Reporting.JsonReportEnabled; + + internal static bool IsArtifactUploadEnabled() + => !IsTruthyEnv(Environment.GetEnvironmentVariable(EnvironmentConstants.DisableArtifactUpload)) + && TUnitSettings.Default.Reporting.ArtifactUploadEnabled; + + internal bool IsHtmlReportEnabledForRun() + { + var resolved = Volatile.Read(ref _htmlReportEnabledAfterDiscovery); + if (resolved != HtmlReportEnabledUnresolved) + { + return resolved == 1; + } + + lock (_htmlReportStateLock) + { + resolved = Volatile.Read(ref _htmlReportEnabledAfterDiscovery); + if (resolved != HtmlReportEnabledUnresolved) + { + return resolved == 1; + } + + var enabled = IsHtmlReportEnabled(); + +#if NET + if (enabled) + { + StartActivityCollection(); + } + else + { + DisposeActivityCollection(); + TraceRegistry.Clear(); + } +#endif + + Volatile.Write(ref _htmlReportEnabledAfterDiscovery, enabled ? 1 : 0); + return enabled; + } + } + // Default HTML report is "{name}-{os}-{tfm}-report.html"; the sidecar drops the // "-report" stem so the default pair reads "{name}-{os}-{tfm}.tunit-report.json". internal static string GetSidecarPath(string htmlOutputPath) @@ -253,7 +465,7 @@ internal static string GetSidecarPath(string htmlOutputPath) internal async Task PublishArtifactAsync(string outputPath, SessionUid sessionUid, CancellationToken cancellationToken) { - if (_messageBus is null) + if (_messageBus is null || !IsArtifactUploadEnabled()) { return; } @@ -270,10 +482,34 @@ internal async Task PublishArtifactAsync(string outputPath, SessionUid sessionUi public void Dispose() { #if NET - _activityCollector?.Dispose(); + DisposeActivityCollection(); #endif } +#if NET + internal bool HasActivityCollector => _activityCollector is not null; + + private void StartActivityCollection() + { + if (_activityCollector is not null) + { + return; + } + + _activityCollector = new ActivityCollector(); + _activityCollector.Start(); + } + + internal void StopActivityCollection() + => _activityCollector?.Stop(); + + private void DisposeActivityCollection() + { + _activityCollector?.Dispose(); + _activityCollector = null; + } +#endif + public string? Filter { get; set; } internal void SetOutputPath(string path) @@ -307,7 +543,7 @@ internal void SetResultsDirectory(string path) internal ReportData BuildReportData() { - var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults"; + var assemblyName = GetAssemblyName(); var tunitVersion = typeof(HtmlReporter).Assembly.GetName().Version?.ToString() ?? "unknown"; // Get the last update with a final state for each test @@ -747,13 +983,16 @@ private static (string Status, ReportExceptionData? Exception, string? SkipReaso private string GetDefaultOutputPath() { - var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults"; + var assemblyName = GetAssemblyName(); var sanitizedName = PathValidator.SanitizeFileName(assemblyName); var os = GetShortOsName(); var tfm = GetShortFrameworkName(); return Path.GetFullPath(Path.Combine(_resultsDirectory, $"{sanitizedName}-{os}-{tfm}-report.html")); } + private static string GetAssemblyName() + => Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults"; + private static string GetShortOsName() { #if NET @@ -857,7 +1096,7 @@ private static bool IsFileLocked(IOException exception) return null; } - if (IsTruthyEnv(Environment.GetEnvironmentVariable(EnvironmentConstants.DisableArtifactUpload))) + if (!IsArtifactUploadEnabled()) { return null; } diff --git a/src/TUnit.Engine/TUnitMessageBus.cs b/src/TUnit.Engine/TUnitMessageBus.cs index 56b89e49902..84d2296af84 100644 --- a/src/TUnit.Engine/TUnitMessageBus.cs +++ b/src/TUnit.Engine/TUnitMessageBus.cs @@ -153,7 +153,16 @@ private static TestNodeStateProperty GetFailureStateProperty(TestContext testCon && testContext.Metadata.TestDetails.Timeout != null && duration >= testContext.Metadata.TestDetails.Timeout.Value) { - return new TimeoutTestNodeStateProperty($"[{categoryLabel}] Test timed out after {testContext.Metadata.TestDetails.Timeout.Value.TotalMilliseconds}ms"); + var explanation = $"[{categoryLabel}] Test timed out after {testContext.Metadata.TestDetails.Timeout.Value.TotalMilliseconds}ms"; + var diagnosticException = unwrapped.InnerException + ?? (unwrapped is OperationCanceledException and not TaskCanceledException ? unwrapped : null); + + if (diagnosticException is not null) + { + explanation = $"{explanation}{Environment.NewLine}{diagnosticException.Message}"; + } + + return new TimeoutTestNodeStateProperty(unwrapped, explanation); } if (category == FailureCategory.Assertion) diff --git a/src/TUnit.Reporting.Tool/Program.cs b/src/TUnit.Reporting.Tool/Program.cs index b05183e5dc0..82caaeb626e 100644 --- a/src/TUnit.Reporting.Tool/Program.cs +++ b/src/TUnit.Reporting.Tool/Program.cs @@ -153,8 +153,17 @@ private static (List Suites, int Skipped) LoadSidecars(string direct // abort the walk — SearchOption.AllDirectories would throw mid-enumeration, before // the per-file guard below ever ran. var enumeration = new EnumerationOptions { RecurseSubdirectories = true, IgnoreInaccessible = true }; - foreach (var file in Directory.EnumerateFiles(directory, "*" + ReportDataJson.SidecarExtension, enumeration)) + var effectiveSidecars = ReportDataJson.SelectEffectiveSidecars( + Directory.EnumerateFiles(directory, "*" + ReportDataJson.SidecarExtension, enumeration)); + foreach (var file in effectiveSidecars) { + // This command is the designated final merge, so no publisher should still + // be active. Skip any suite whose stable publication lock is still held. + if (ReportDataJson.IsSidecarPublicationInProgress(file)) + { + continue; + } + byte[] bytes; try { @@ -168,6 +177,11 @@ private static (List Suites, int Skipped) LoadSidecars(string direct continue; } + if (ReportDataJson.IsSidecarExcluded(file, bytes)) + { + continue; + } + if (!seenDigests.Add(Convert.ToBase64String(sha.ComputeHash(bytes)))) { continue; diff --git a/tests/TUnit.Assertions.Tests/TypeAssertionTests.cs b/tests/TUnit.Assertions.Tests/TypeAssertionTests.cs index 6d8cb5107a4..81f3698939e 100644 --- a/tests/TUnit.Assertions.Tests/TypeAssertionTests.cs +++ b/tests/TUnit.Assertions.Tests/TypeAssertionTests.cs @@ -1,10 +1,15 @@ +using System.Reflection; +using TUnit.Assertions.Core; using TUnit.Assertions.Extensions; -using TUnit.Assertions.Extensions; +using TUnit.Assertions.Sources; namespace TUnit.Assertions.Tests; public class TypeAssertionTests { + private class Animal { } + private class Dog : Animal { } + // Test types for various scenarios private class TestClass { } private interface ITestInterface { } @@ -459,6 +464,109 @@ public async Task Test_Type_IsNotCOMObject_TestClass() await Assert.That(type).IsNotCOMObject(); } + [Test] + public async Task Test_Type_IsAssignableTo_Generic_UsesRepresentedType() + { + Type? result = await Assert.That(typeof(Dog)).IsAssignableTo(); + + await Assert.That(result).IsSameReferenceAs(typeof(Dog)); + } + + [Test] + public async Task Test_Type_IsAssignableTo_Generic_RetainsTypeForChaining() + { + await Assert.That(typeof(Dog)) + .IsAssignableTo() + .And.IsClass(); + } + + [Test] + public async Task Test_TypeInfo_GenericAssignability_UsesRepresentedType() + { + System.Reflection.TypeInfo animalType = typeof(Animal).GetTypeInfo(); + System.Reflection.TypeInfo dogType = typeof(Dog).GetTypeInfo(); + + await Assert.That(dogType).IsAssignableTo(); + await Assert.That(dogType).IsNotAssignableTo(); + await Assert.That(animalType).IsAssignableFrom(); + await Assert.That(dogType).IsNotAssignableFrom(); + } + + [Test] + public async Task Test_Type_IsAssignableTo_GenericSource_RetainsRuntimeTypeSemantics() + { + IAssertionSource source = new TypeValueAssertion(typeof(Dog), null); + + await Assert.That(async () => await source.IsAssignableTo()) + .Throws(); + } + + [Test] + public async Task Test_Type_IsAssignableTo_AfterAnd_RetainsRuntimeTypeSemantics() + { + var action = async () => await Assert.That(typeof(Dog)) + .IsClass() + .And.IsAssignableTo(); + + await Assert.That(action).Throws(); + } + + [Test] + public async Task Test_Type_OtherGenericSources_RetainRuntimeTypeSemantics() + { + IAssertionSource source = new TypeValueAssertion(typeof(Animal), null); + + await source.IsNotAssignableTo(); + await Assert.That(async () => await source.IsAssignableFrom()) + .Throws(); + await source.IsNotAssignableFrom(); + } + + [Test] + public async Task Test_Type_DirectGenericAssertions_UseRepresentedType() + { + await Assert.That(typeof(Animal)).IsNotAssignableTo(); + await Assert.That(typeof(Animal)).IsAssignableFrom(); + await Assert.That(async () => await Assert.That(typeof(Animal)).IsNotAssignableFrom()) + .Throws(); + } + + [Test] + public async Task Test_Type_IsAssignableFrom_Generic_UsesRepresentedType() + { + await Assert.That(typeof(Animal)).IsAssignableFrom(); + } + + [Test] + public async Task Test_Type_IsAssignableTo_RuntimeTypeOverload_Passes() + { + await Assert.That(typeof(Dog)).IsAssignableTo(typeof(Animal)); + } + + [Test] + public async Task Test_Type_IsAssignableFrom_RuntimeTypeOverload_Passes() + { + await Assert.That(typeof(Animal)).IsAssignableFrom(typeof(Dog)); + } + + [Test] + public async Task Test_Type_IsAssignableTo_NullRuntimeType_FailsAssertion() + { + var action = async () => await Assert.That(typeof(Dog)).IsAssignableTo(null!); + + var exception = await Assert.That(action).Throws(); + await Assert.That(exception.Message).Contains("expected type was null"); + } + + [Test] + public async Task Test_Type_IsAssignableFrom_NullRuntimeType_FailsAssertion() + { + var action = async () => await Assert.That(typeof(Animal)).IsAssignableFrom(null!); + + var exception = await Assert.That(action).Throws(); + await Assert.That(exception.Message).Contains("source type was null"); + } + #if NET5_0_OR_GREATER // IsByRefLike / IsNotByRefLike (NET5+) [Test] diff --git a/tests/TUnit.DocTests/GlobalUsings.cs b/tests/TUnit.DocTests/GlobalUsings.cs index 5294907a6f8..64b5c1f3605 100644 --- a/tests/TUnit.DocTests/GlobalUsings.cs +++ b/tests/TUnit.DocTests/GlobalUsings.cs @@ -2,33 +2,74 @@ global using System.Collections; global using System.Collections.Concurrent; global using System.Collections.Generic; +global using System.Collections.ObjectModel; global using System.Data; global using System.Diagnostics; global using System.Diagnostics.CodeAnalysis; global using System.Linq; +global using System.IO; +global using System.Data.Common; global using System.Net; global using System.Net.Http; +global using System.Net.Http.Json; +global using System.Net.Http.Headers; +global using System.Net.Sockets; +global using System.Reflection; global using System.Runtime.CompilerServices; global using System.Runtime.InteropServices; +global using System.Text; +global using System.Text.Json; +global using System.Threading.Channels; +global using System.ComponentModel.DataAnnotations; global using System.Text.RegularExpressions; global using System.Threading; global using System.Threading.Tasks; +global using System.Reactive.Linq; +global using System.Reactive.Subjects; +global using System.Reactive.Threading.Tasks; global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Mvc.Testing; +global using Microsoft.AspNetCore.Mvc; +global using Microsoft.EntityFrameworkCore; +global using Microsoft.EntityFrameworkCore.Infrastructure; +global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Logging; +global using Microsoft.Extensions.Hosting; +global using Grpc.Core; +global using Grpc.Core.Interceptors; +global using MassTransit; +global using Aspire.Hosting; +global using Aspire.Hosting.ApplicationModel; +global using Aspire.Hosting.Testing; +global using AutoFixture; +global using AutoFixture.Kernel; +global using CliWrap; +global using CliWrap.Buffered; +global using Npgsql; global using Microsoft.Playwright; +global using OpenTelemetry; +global using OpenTelemetry.Exporter; +global using OpenTelemetry.Trace; +global using OpenTelemetry.Logs; global using TUnit.Assertions; global using TUnit.Assertions.Attributes; global using TUnit.Assertions.Core; global using TUnit.Assertions.Extensions; +global using TUnit.Assertions.Enums; +global using static TUnit.DocTests.DocValues; global using TUnit.Assertions.Should.Attributes; global using TUnit.Assertions.Should.Extensions; +global using TUnit.Assertions.Should; global using TUnit.AspNetCore; +global using TUnit.AspNetCore.Extensions; global using TUnit.Aspire; global using TUnit.Core; global using TUnit.Core.Enums; global using TUnit.Core.Extensions; +global using TUnit.Core.Exceptions; global using TUnit.Core.Interfaces; global using TUnit.FsCheck; global using TUnit.Mocks; @@ -42,3 +83,10 @@ global using TUnit.OpenTelemetry; global using TUnit.Playwright; global using TUnit.Core.Executors; +global using DotNet.Testcontainers.Containers; +global using DotNet.Testcontainers.Networks; +global using DotNet.Testcontainers.Builders; +global using Testcontainers.Kafka; +global using Testcontainers.PostgreSql; +global using Testcontainers.Redis; +global using StackExchange.Redis; diff --git a/tests/TUnit.DocTests/Program.cs b/tests/TUnit.DocTests/Program.cs index bed7b17c914..7142c03a523 100644 --- a/tests/TUnit.DocTests/Program.cs +++ b/tests/TUnit.DocTests/Program.cs @@ -1,20 +1,140 @@ namespace TUnit.DocTests; -#pragma warning disable CS0169, CS0414 +public sealed class DocumentationWebApplicationFactory : TestWebApplicationFactory +{ +} -internal abstract class SnippetContext +public abstract class SnippetContext : WebApplicationTest { - protected static readonly CancellationToken cancellationToken = default; - protected static readonly CancellationToken ct = default; + protected static CancellationToken cancellationToken => default; + protected static CancellationToken ct => default; } public sealed class Program { - public static void Main() +} + +public interface IEntity +{ + TId Id { get; } +} + +public interface IValidatable +{ + bool IsValid(); +} + +public sealed record User(string Name = "Alice") : IEntity +{ + public object Id { get; init; } = 0; + public string Role { get; init; } = "User"; + public string Email { get; init; } = "alice@example.com"; + public int Age { get; init; } = 30; + public string FirstName { get; init; } = "Alice"; + public string LastName { get; init; } = "Example"; + public string[] Permissions { get; init; } = ["read"]; + public string[] Roles { get; init; } = ["Admin"]; + public bool IsActive { get; init; } = true; + public DateTime CreatedDate { get; init; } = DateTime.UtcNow; + + public bool HasPermission(string permission) => Permissions.Contains(permission); + public bool CanRead => true; + public bool CanWrite => true; + public bool CanDelete => false; + + Guid IEntity.Id => Id is Guid id ? id : Guid.Empty; +} + +public sealed class Calculator +{ + public int Add(int left, int right) => left + right; + public int Multiply(int left, int right) => left * right; +} + +public class AppFixture : AspireFixture +{ +} + +public sealed class ApiClientFixture +{ + public HttpClient Client { get; } = new(); +} + +public sealed class RedisFixture +{ + public StackExchange.Redis.IDatabase Database => throw new NotImplementedException(); +} + +public sealed class InMemoryDatabase : ITestDatabase +{ + public PostgreSqlContainer Container => throw new NotImplementedException(); + public void Initialize() { } + public Task InitializeAsync() => Task.CompletedTask; + public void Dispose() { } +} + +public class WebApplicationFactory : TestWebApplicationFactory +{ + public DatabaseConnection Database { get; } = new(); +} + +public class WebAppFactory : TestWebApplicationFactory +{ +} + +public class SharedFactory : TestWebApplicationFactory +{ +} + +public class EfCoreWebApplicationFactory : TestWebApplicationFactory +{ +} + +public abstract class TestsBase : WebApplicationTest +{ +} + +public sealed record Todo(int Id = 1, string Title = "Example"); + +public sealed class Counter : Microsoft.AspNetCore.Components.ComponentBase +{ + protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { + builder.OpenElement(0, "p"); + builder.AddContent(1, "Current count: 0"); + builder.CloseElement(); } } -internal sealed record User(string Name = "Alice"); +public sealed class MyRequest +{ + public string Payload { get; init; } = string.Empty; + public string TestId { get; init; } = string.Empty; +} + +public sealed record OrderMessage; + +public static class Projects +{ + public class MyAppHost : global::Aspire.Hosting.IProjectMetadata + { + public string ProjectPath => "MyAppHost.csproj"; + public global::Aspire.Hosting.LaunchSettings? LaunchSettings => null; + public bool IsFileBasedApp => false; + public bool SuppressBuild => true; + } + + public sealed class AppHostA : MyAppHost + { + } + + public sealed class AppHostB : MyAppHost + { + } + + public sealed class MyApp_AppHost : MyAppHost + { + } +} -internal sealed record Person(string Name = "Alice", int Age = 42); +public sealed record Person(string Name = "Alice", int Age = 42); diff --git a/tests/TUnit.DocTests/SupportTypes.cs b/tests/TUnit.DocTests/SupportTypes.cs new file mode 100644 index 00000000000..13aefdb91c1 --- /dev/null +++ b/tests/TUnit.DocTests/SupportTypes.cs @@ -0,0 +1,821 @@ +using TUnit.Core.Interfaces; + +namespace TUnit.DocTests +{ + public static class DocValues + { + public static int actual => 1; + public static int expected => 1; + public static int value => 1; + public static int actualValue => 1; + public static int expectedValue => 1; + public static int age => 30; + public static int score => 100; + public static double temperature => 21.5; + public static bool condition => true; + public static DateTime futureDate => DateTime.UtcNow.AddDays(1); + public static bool isValid => true; + public static int[] collection => [1, 2, 3]; + public static int item => 1; + public static string text => "example"; + public static string pattern => "example"; + public static string prefix => "ex"; + public static string suffix => "ple"; + public static string substring => "amp"; + public static string email => "alice@example.com"; + public static string filename => "example.txt"; + public static string input => "example"; + public static string message => "example"; + public static string username => "alice"; + public static int[] items => [1, 2, 3]; + public static int[] list => [1, 2, 3]; + public static int[] numbers => [1, 2, 3]; + public static int[] otherNumbers => [3, 2, 1]; + public static object[] objects => [new(), new()]; + public static int[] values => [1, 2, 3]; + public static Task longRunningTask => Task.CompletedTask; + public static object obj => new(); + public static object? optional => null; + public static Order order => new(); + public static object other => new(); + public static Person person => new(); + public static int result => 1; + public static HttpStatusCode statusCode => HttpStatusCode.OK; + public static User user => user1; + public static User[] users => GetUsers(); + public static Product[] products => [new Product()]; + public static object someObject => new(); + public static object obj1 => new(); + public static object obj2 => obj1; + public static object obj3 => new(); + public static int value1 => 1; + public static int value2 => 2; + public static HttpClient _httpClient => new(); + public static HttpClient _client => new(); + public static AppFixture fixture => new(); + public static WebApplicationBuilder builder => WebApplication.CreateBuilder(); + public static DatabaseConnection Database => new(); + public static DatabaseConnection _connection => new(); + public static string[] _queries => ["SELECT 1"]; + public static IContainer _container => new ContainerBuilder("alpine:3.23").Build(); + public static WebApplicationFactory _factory => new(); + public static HttpClient Client => new(); + public static HttpResponseMessage response => new(HttpStatusCode.OK); + public static ExampleFactory Factory => new(); + public static TestContext context => TestContext.Current!; + public static TestContext testContext => TestContext.Current!; + public static CancellationToken externalToken => default; + public static string ConnectionString => "Server=localhost"; + public static string connectionString => "Host=localhost;Database=examples;Username=postgres;Password=postgres"; + public static IServiceCollection services => new ServiceCollection(); + public static Microsoft.Extensions.Configuration.IConfiguration Configuration => + new ConfigurationBuilder().AddInMemoryCollection().Build(); + public static ExampleRepository _repository => new(); + public static ConcurrentDictionary _cache => new(); + public static IPage _page => null!; + public static IBrowserContext _browserContext => null!; + public static TestWebApplicationFactory myExistingFactory => null!; + public static object? _value; + public static object? _response; + public static IDisposable _resource = new Connection(); + public static AsyncLocal _myAsyncLocal = new(); + public static User validUser => user; + public static string[] results => ["result"]; + public static Order invalidOrder => new(); + public static Mock mock => Mock.Of(); + public static Mock mockLogger => mock; + public static Mock mockRepo => mock; + public static IUniversalService svc => mock.Object; + public static MockHttpClient client => Mock.HttpClient("https://example.com"); + public static MockHttpHandler handler => client.Handler; + public static MockLogger logger => Mock.Logger(); + public static MyService myService => new(); + public static string requestId => "request-1"; + public static Func eventHandler => (_, _) => ValueTask.CompletedTask; + public static User user1 => new("Alice"); + public static User user2 => new("Bob"); + public static User user3 => new("Charlie"); + public static User alice => user1; + public static User bob => user2; + public static User charlie => user3; + public static User[] expected1 => [user1]; + public static User[] expected2 => [user2]; + public static ExampleDatabase database => new(); + public static ExampleApi _api => new(); + public static ExampleApi Api => new(); + public static MessagePump messagePump => new(); + + public static void DoSomething() { } + public static Task DoSomethingAsync() => Task.CompletedTask; + public static void DivideByZero() => throw new DivideByZeroException(); + public static Task FetchDataAsync() => Task.FromResult("data"); + public static int Add(int left, int right) => left + right; + public static Animal GetAnimal() => new Dog(); + public static T GetService() where T : class => (T)(object)new ExampleUserService(); + public static Type LoadPluginType() => typeof(MyService); + public static Task GetAnimalAsync() => Task.FromResult(new Dog()); + public static Dog GetDog() => new(); + public static Task GetUserAsync() => Task.FromResult(user); + public static Task GetUserAsync(int id) => Task.FromResult(user); + public static Task GetUserAsync(string id) => Task.FromResult(user); + public static Task GetProductAsync() => Task.FromResult(new Product()); + public static Task GetProductAsync(int id) => Task.FromResult(new Product()); + public static ExampleConfiguration LoadConfiguration() => new(); + public static ExampleConfiguration LoadConfiguration(string environment) => new(); + public static TestResult GetCurrentResult() => TestContext.Current!.Execution.Result!; + public static string GetInput() => "input"; + public static bool GetOptionalFlag() => true; + public static Task PerformOperation() => Task.FromResult(new()); + public static Task PerformOperationAsync() => Task.CompletedTask; + public static Task LongRunningOperationAsync() => Task.CompletedTask; + public static Task LongRunningOperationAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public static Task FailingOperationAsync() => Task.FromException(new InvalidOperationException()); + public static Task SuccessfulOperationAsync() => Task.CompletedTask; + public static void SafeOperation() { } + public static int ProcessValue(int input) => input; + public static bool ValidateInput(object? input) => input is not null; + public static bool ValidateEmail(string address) => true; + public static bool ValidateUser(User candidate) => true; + public static bool EmailValidator(string address) => true; + public static bool Calculate(int input) => true; + public static int Calculate(int left, int right) => left + right; + public static bool SomeLogic() => true; + public static bool SomeLogic(int first, int second) => first == second; + public static bool SomeLogic(int value, string text) => value > 0 && text.Length > 0; + public static void ThrowsException() => throw new ArgumentException("example", "paramName"); + public static Task ThrowsExceptionAsync() => Task.FromException(new InvalidOperationException()); + public static Task InsertDuplicateAsync() => Task.FromException(new DbUpdateException("duplicate key")); + public static IServiceProvider ConfigureServices() => new ServiceCollection().BuildServiceProvider(); + public static void ProcessData() { } + public static void ProcessData(object? data) { } + public static Task ProcessDataAsync() => Task.CompletedTask; + public static Task ProcessDataAsync(User value) => Task.CompletedTask; + public static Task ProcessDataAsync(object value) => Task.CompletedTask; + public static Task ProcessDataInBackgroundAsync() => Task.CompletedTask; + public static void ProcessInvalidData() => throw new ValidationException(); + public static void ProcessInvalidData(User value) => throw new ValidationException(); + public static void ProcessInvalidData(object? value) => throw new ArgumentNullException("data"); + public static void RiskyOperation() => throw new InvalidOperationException(); + public static Task RiskyOperationAsync() => Task.FromException(new InvalidOperationException()); + public static bool PerformCheck() => true; + public static bool CheckIfExpired() => false; + public static bool CheckIfExpired(DateTime value) => value < DateTime.UtcNow; + public static bool IsNormallyDistributed(IEnumerable? measurements) => measurements is not null; + public static Task ParallelOperationAsync() => Task.CompletedTask; + public static Task RetryAsync() => Task.CompletedTask; + public static async Task RetryAsync(Func> action, int maxRetries) => await action(); + public static Task ConsumeItemsAsync() => Task.CompletedTask; + public static async Task ConsumeItemsAsync(ChannelReader reader) + { + await foreach (var value in reader.ReadAllAsync()) { _ = value; } + } + public static async Task ProduceItemsAsync(ChannelWriter writer) + { + await writer.WriteAsync(1); + writer.Complete(); + } + public static async IAsyncEnumerable ProduceItemsAsync() + { + yield return 1; + await Task.CompletedTask; + } + public static Task LoadTestCasesFromDatabaseAsync() => Task.FromResult(new[] { "case" }); + public static Task LoadUserAsync() => Task.FromResult(user); + public static Task LoadUserAsync(int id) => Task.FromResult(user); + public static Task LoadFromDatabaseAsync() => Task.FromResult(users); + public static Task LoadFromDatabaseAsync(IEnumerable ids) => Task.FromResult(users); + public static Task LoadFromDatabaseAsync(int id) => Task.FromResult(true); + public static Task GetTeamAsync() => Task.FromResult(new Team()); + public static Task GetOrderAsync() => Task.FromResult(order); + public static Task GetOrdersAsync() => Task.FromResult(new[] { order }); + public static Task GetDataAsync() => Task.FromResult("data"); + public static Task GetValueAsync() => Task.FromResult("value"); + public static string GetValue() => "value"; + public static string GetUsername() => username; + public static string GetRequiredValue() => "value"; + public static string? GetOptionalString() => null; + public static int? GetOptionalValue() => null; + public static bool GetFlag() => true; + public static bool GetFeatureFlag() => true; + public static Task GetFeatureFlag(string name) => Task.FromResult(true); + public static User GetCurrentUser() => user; + public static int GetCount() => 1; + public static Task GetCompanyAsync() => Task.FromResult(new Company()); + public static Task GetCustomerAsync() => Task.FromResult(new Customer()); + public static Task GetMeasurementsAsync() => Task.FromResult(new[] { 1.0 }); + public static Dictionary GetFileMetadata() => new() + { + ["ContentType"] = "text/plain", ["Size"] = 1L, ["LastModified"] = DateTime.UtcNow + }; + public static Dictionary GetConfigurationValues() => new() + { + ["DatabaseConnection"] = "localhost", ["ApiKey"] = "key", ["Environment"] = "Test", ["Database"] = "examples" + }; + public static Task GetAuthTokenAsync() => Task.FromResult("token"); + public static Task CalculateAsync() => Task.FromResult(1); + public static Task CalculateAsync(int left, int right) => Task.FromResult(left + right); + public static int CalculateResult() => 1; + public static double CalculateStandardDeviation(IEnumerable values) => 0; + public static int ComputeValue() => 1; + public static Task SimulateAsyncWork() => Task.FromResult("complete"); + public static Task SomeLongRunningOperation() => Task.CompletedTask; + public static Task SomeLongRunningOperation(CancellationToken cancellationToken) => Task.CompletedTask; + public static Task NotifyTestFinished() => Task.CompletedTask; + public static Task LogMetrics() => Task.CompletedTask; + public static Task LogMetrics(TestContext context) => Task.CompletedTask; + public static Task CaptureScreenshot() => Task.CompletedTask; + public static Task CaptureScreenshot(string path) => Task.CompletedTask; + public static Task CaptureScreenshot(CancellationToken cancellationToken) => Task.CompletedTask; + public static Task SaveScreenshot(string name) => Task.CompletedTask; + public static Task IsServiceAvailable() => Task.FromResult(true); + public static Task ProcessRequest() => Task.CompletedTask; + public static Task ProcessContentAsync() => Task.CompletedTask; + public static Task ProcessContentAsync(string content) => Task.CompletedTask; + public static void ProcessContent() { } + public static void ProcessContent(string content) { } + public static Task ProcessOrder() => Task.CompletedTask; + public static Task ProcessOrder(OrderMessage message) => Task.CompletedTask; + public static Task DoWork(MyRequest request) => Task.CompletedTask; + public static Task SaveUserAsync() => Task.CompletedTask; + public static Task SaveUserAsync(User value) => Task.CompletedTask; + public static Task SaveUsersBatchAsync() => Task.CompletedTask; + public static Task SaveUsersBatchAsync(IEnumerable values) => Task.CompletedTask; + public static Task SeedTestDataAsync() => Task.CompletedTask; + public static Task SeedTestDataAsync(string connectionString) => Task.CompletedTask; + public static Task RunMigrationsAsync() => Task.CompletedTask; + public static Task RunMigrationsAsync(string connectionString) => Task.CompletedTask; + public static Task CreateTableAsync(string name) => Task.CompletedTask; + public static Task CreateQueueAsync(string name) => Task.CompletedTask; + public static Task CreateSchemaAsync(string name) => Task.CompletedTask; + public static Task CreateSchemaAsync() => Task.CompletedTask; + public static Task CleanupTestDataAsync() => Task.CompletedTask; + public static Task QueryDatabase() => Task.FromResult("database.log"); + public static Task ExecuteHttpRequests() => Task.FromResult("http.log"); + public static Task CollectTraces() => Task.FromResult("traces.log"); + public static Task BackgroundLoopAsync() => Task.CompletedTask; + public static string CollectEnvironmentInfo() => "environment"; + public static void StartLogging() { } + public static void StartLogging(string path) { } + public static Task StopRecording() => Task.FromResult("recording.webm"); + public static void GenerateReport() { } + public static void GenerateReport(string path) { } + public static bool CheckDatabaseConnection() => true; + public static bool CheckExternalService() => true; + public static Task CheckApiAvailability() => Task.FromResult(true); + public static Task CallApi() => Task.CompletedTask; + public static User[] GetSortedList1() => [alice, bob]; + public static User[] GetSortedList2() => [bob, charlie]; + public static User[] GetUsers() => [alice, bob, charlie]; + public static Task GetAllUsersFromDatabase() => Task.FromResult(users); + public static Task GetUserCountFromDatabase() => Task.FromResult(users.Length); + public static Task BeginTransactionAsync() => Task.FromResult(new ExampleTransaction()); + public static Task CreateUserAsync(string name) => Task.FromResult(new User(name)); + public static Task CreateRecordAsync() => Task.FromResult(new ExampleRecord(DateTime.UtcNow)); + public static Person? FindPerson(string id) => null; + public static User CreateUser() => user; + public static DependencyService CreateService() => new(); + public static ExampleToken CreateToken() => new(DateTime.UtcNow.AddHours(1)); + public static ExampleToken CreateExpiredToken() => new(DateTime.UtcNow.AddHours(-1)); + public static Task AddToBag() => Task.FromResult("item-1"); + public static Task DeleteFromBag(object itemId) => Task.CompletedTask; + } + + public interface IUniversalService + { + event Action? OnMessage; + string Name { get; set; } + int Count { get; set; } + int Add(int left, int right); + int Compute(int one, int two, int three, int four, int five); + void Delete(int id); + string Format(string value); + User[] GetByRole(string role); + string GetConfig(); + bool GetRole(string role); + User GetUser(int id); + Task GetUserAsync(int id); + string GetValue(string key); + string Greet(string name); + void Log(params object[] values); + int Multiply(int left, int right); + string Process(string value); + string Process(int value); + bool ProcessItems(List values); + string[] Search(string query); + string[] Search(string query, int limit); + bool SendEmail(string address, string subject, string body); + void SendMessage(string message); + Task SaveAsync(object value); + bool SetAge(int age); + void SetState(string state); + int Sum(params int[] values); + void Swap(ref int left, ref int right); + bool TryGet(string key, out string value); + } + + public sealed class Sut + { + private readonly IHttpClientFactory _factory; + + public Sut(IHttpClientFactory factory) => _factory = factory; + + public Task DoWork() + { + _ = _factory.CreateClient(); + return Task.CompletedTask; + } + } + + public abstract class Animal { } + public sealed class Dog : Animal { } + public sealed class MyClass { } + public sealed record ExampleToken(DateTime ExpiresAt); + public sealed record ExampleRecord(DateTime CreatedAt); + public sealed record ChargeResult(bool Success); + public sealed record Cart(decimal Total); + public interface IPaymentGateway + { + Task ChargeAsync(decimal amount); + } + public sealed class CheckoutService + { + private readonly IPaymentGateway? _paymentGateway; + + public CheckoutService() { } + public CheckoutService(IPaymentGateway paymentGateway) => _paymentGateway = paymentGateway; + + public Task ApplyDiscountAsync(Order order, string code) => Task.FromResult(order.Total); + public Task ApplyDiscountAsync(string tier, double subtotal) => + Task.FromResult(tier == "GOLD" ? subtotal * 0.8 : subtotal * 0.9); + public Task CompleteAsync(Cart cart) => + _paymentGateway?.ChargeAsync(cart.Total) ?? Task.FromResult(new ChargeResult(false)); + } + public sealed class AuthConfig { public string Token { get; init; } = string.Empty; } + public interface ITestHelper { } + public sealed class TestHelper : ITestHelper { } + public sealed record ProductResponse(int Id = 1, string Name = "Widget"); + public interface ITestDataSeeder { Task SeedAsync(); } + public sealed class Cat : Animal { } + public readonly record struct Point(int X, int Y); + public sealed record Circle(double Radius = 1); + public interface IMovable { } + public interface IService + { + event Action? OnMessage; + User GetUser(int id); + string GetName(); + int GetCount(); + } + public interface IMyService : IService { } + public interface IUserService + { + User GetUser(int id); + User[] GetByRole(string role); + User[] Search(string name, int page); + } + public sealed class ExampleUserService : IUserService + { + public User GetUser(int id) => DocValues.user; + public User[] GetByRole(string role) => DocValues.users; + public User[] Search(string name, int page) => DocValues.users; + } + public sealed class ExampleFactory { public object Create(string name) => new UserService(new DatabaseConnection()); } + public static class DatabaseQuery { public static User[] GetAllUsers() => DocValues.users; } + public class ProductionService + { + public virtual TestConfig GetConfig() => new(); + public virtual void DoWork() { } + } + public sealed class TestConfig { } + public static class TestDatabase + { + public static Task SeedAsync() => Task.CompletedTask; + public static Task ResetAsync() => Task.CompletedTask; + public static Task CloseConnectionsAsync() => Task.CompletedTask; + } + public sealed record ServiceCallResult(string Status); + public static class FlakyService + { + public static Task CallAsync() => Task.FromResult(new ServiceCallResult("OK")); + public static Task CallAsync(CancellationToken cancellationToken) => + Task.FromResult(new ServiceCallResult("OK")); + } + public interface IGreeter + { + string Greet(string name); + } + public interface IConnection + { + event EventHandler? OnMessage; + } + public interface IInvocationFeatures + { + T? Get(); + } + public interface IFunctionBindingsFeature + { + object? InvocationResult { get; } + } + public interface IEntity + { + string Name { get; set; } + int Count { get; set; } + bool TryGet(string key, out string value); + void Swap(ref int value); + } + public class Entity : IEntity + { + public Guid Id { get; init; } = Guid.NewGuid(); + public string Name { get; set; } = string.Empty; + public int Count { get; set; } + public bool TryGet(string key, out string value) { value = "found-value"; return true; } + public void Swap(ref int value) => value = 99; + } + public class BaseRepository { } + public sealed class DataTransferObject { } + public sealed class AsyncResource : IAsyncDisposable + { + public bool IsDisposed { get; private set; } + public ValueTask DisposeAsync() { IsDisposed = true; return ValueTask.CompletedTask; } + } + public sealed class Connection : IDisposable + { + public void SendData(string data) => throw new InvalidOperationException("not connected"); + public void Dispose() { } + } + public sealed class Container : IDisposable + { + public void Dispose() { } + } + public sealed class ExpensiveCalculator + { + public int? CachedResult { get; private set; } + public int GetResult() => CachedResult ??= 42; + } + public sealed class RateLimiter(int maxRequests, TimeSpan perTimeSpan) + { + public int MaxRequests { get; } = maxRequests; + public TimeSpan PerTimeSpan { get; } = perTimeSpan; + public Task ExecuteAsync(Func action) => action(); + } + public sealed class CircuitBreaker + { + public Task ExecuteAsync(Func action) => action(); + } + public sealed class CircuitBreakerOpenException : Exception { } + public sealed class Workflow + { + public WorkflowState CurrentState { get; private set; } + public Task StartAsync() { CurrentState = WorkflowState.Started; return Task.CompletedTask; } + } + public enum WorkflowState { NotStarted, Started } + public sealed class Service { public Service() { } public Service(object dependency) { } } + public class MyService + { + public MyService() { } + public MyService(string connectionString, int timeout) { } + public MyService(Microsoft.Extensions.Logging.ILogger logger) { } + public MyService(Microsoft.Extensions.Logging.ILogger logger) { } + public void Start() { } + public Task ProcessAsync(string id) => Task.CompletedTask; + public Task SendAsync(MyRequest request) => Task.CompletedTask; + public DatabaseConnection? Connection { get; private set; } + public Task InitializeAsync() { Connection = new DatabaseConnection(); return Task.CompletedTask; } + } + public sealed class ExampleBackgroundService + { + public bool IsRunning { get; private set; } + public Task StartAsync() { IsRunning = true; return Task.CompletedTask; } + } + public sealed class ExampleTransaction : IDisposable + { + public Task RollbackAsync() => Task.CompletedTask; + public void Dispose() { } + } + public static class CacheService + { + public static Task WarmUpAsync() => Task.CompletedTask; + public static bool IsWarmedUp => true; + public static bool IsWarmed => true; + } + public static class FeatureFlags + { + public static Task LoadAsync() => Task.CompletedTask; + public static Task ResetAllAsync() => Task.CompletedTask; + public static Task CountAsync() => Task.FromResult(0); + public static bool IsEnabled(string name) => true; + } + public static class ReportingService + { + public static ValueTask ReportTestStarted(params object?[] values) => ValueTask.CompletedTask; + public static ValueTask ReportTestCompleted(params object?[] values) => ValueTask.CompletedTask; + } + public static class TelemetryClient + { + public static void TrackMetric(string name, double value) { } + } + public enum ProductStatus { Pending, Active, Preview, Inactive } + public enum OrderStatus { Pending, Completed } + + public sealed class ExampleDatabase + { + public Task GetActiveUsersAsync() => Task.FromResult(DocValues.GetUsers()); + public Task GetUsersSortedByNameAsync() => Task.FromResult(DocValues.GetUsers()); + } + + public sealed class ExampleApi + { + public Task GetUsersAsync() => Task.FromResult(DocValues.GetUsers()); + public Task PingAsync() => Task.FromResult(new PingResult(true)); + } + public sealed record PingResult(bool IsSuccess); + public sealed record PaymentResult(bool Success); + public static class OrderRepository + { + public static Task CreateAsync(string item) => Task.FromResult(new Order()); + } + public static class PaymentApi + { + public static Task ChargeAsync(decimal amount) => Task.FromResult(new PaymentResult(true)); + } + public sealed class MessagePump + { + public event Action? ShuttingDown; + public Task RunAsync(CancellationToken cancellationToken) { ShuttingDown?.Invoke(); return Task.CompletedTask; } + } + public sealed class MyCustomProcessor : BaseProcessor + { + public override void OnEnd(Activity data) { } + } + + public sealed class ExampleConfiguration + { + public bool IsValid => true; + public DatabaseSettings DatabaseConnection { get; } = new(); + public bool EnableAdvancedFeatures => false; + public bool EnableNewFeature => true; + public bool EnableBetaFeature => false; + public object? AdvancedSettings => null; + } + + public sealed class DatabaseSettings + { + public string Server { get; init; } = "localhost"; + public string Database { get; init; } = "examples"; + } + + public sealed class Address + { + public string Street { get; init; } = "1 Main Street"; + public string City { get; init; } = "Seattle"; + public string ZipCode { get; init; } = "98101"; + } + + public sealed class Team + { + public string Name { get; init; } = "Team Alpha"; + public User[] Members { get; init; } = DocValues.users; + public DateTime CreatedDate { get; init; } = DateTime.UtcNow; + } + + public sealed class Company + { + public string Name { get; init; } = "TechCorp"; + public Address Address { get; init; } = new(); + public User[] Employees { get; init; } = DocValues.users; + } + + public sealed class ExampleRepository + { + public Task FindByIdAsync(string id) => Task.FromResult(id == "valid-id" ? new() : null); + } + + public sealed class DependencyService + { + public object Logger { get; } = new(); + public object Repository { get; } = new(); + public object Cache { get; } = new(); + } + + public sealed class WindowsOnlyAttribute() : SkipAttribute("Windows only") + { + public override Task ShouldSkip(TestRegisteredContext context) => Task.FromResult(false); + } + + public sealed class AssignTestIdentifiersAttribute : Attribute, ITestDiscoveryEventReceiver + { + public ValueTask OnTestDiscovered(DiscoveredTestContext context) => ValueTask.CompletedTask; + } + + public sealed class MyParallelLimit : IParallelLimit + { + public int Limit => Environment.ProcessorCount; + } + + public sealed class TimingTestExecutor : ITestExecutor + { + public ValueTask ExecuteTest(TestContext context, Func action) => action(); + } + + public sealed class LoggingHookExecutor : IHookExecutor + { + public ValueTask ExecuteBeforeTestDiscoveryHook(MethodMetadata method, BeforeTestDiscoveryContext context, Func action) => action(); + public ValueTask ExecuteBeforeTestSessionHook(MethodMetadata method, TestSessionContext context, Func action) => action(); + public ValueTask ExecuteBeforeAssemblyHook(MethodMetadata method, AssemblyHookContext context, Func action) => action(); + public ValueTask ExecuteBeforeClassHook(MethodMetadata method, ClassHookContext context, Func action) => action(); + public ValueTask ExecuteBeforeTestHook(MethodMetadata method, TestContext context, Func action) => action(); + public ValueTask ExecuteAfterTestDiscoveryHook(MethodMetadata method, TestDiscoveryContext context, Func action) => action(); + public ValueTask ExecuteAfterTestSessionHook(MethodMetadata method, TestSessionContext context, Func action) => action(); + public ValueTask ExecuteAfterAssemblyHook(MethodMetadata method, AssemblyHookContext context, Func action) => action(); + public ValueTask ExecuteAfterClassHook(MethodMetadata method, ClassHookContext context, Func action) => action(); + public ValueTask ExecuteAfterTestHook(MethodMetadata method, TestContext context, Func action) => action(); + } + + public sealed class MyCustomExecutor : IHookExecutor + { + public ValueTask ExecuteBeforeTestDiscoveryHook(MethodMetadata method, BeforeTestDiscoveryContext context, Func action) => action(); + public ValueTask ExecuteBeforeTestSessionHook(MethodMetadata method, TestSessionContext context, Func action) => action(); + public ValueTask ExecuteBeforeAssemblyHook(MethodMetadata method, AssemblyHookContext context, Func action) => action(); + public ValueTask ExecuteBeforeClassHook(MethodMetadata method, ClassHookContext context, Func action) => action(); + public ValueTask ExecuteBeforeTestHook(MethodMetadata method, TestContext context, Func action) => action(); + public ValueTask ExecuteAfterTestDiscoveryHook(MethodMetadata method, TestDiscoveryContext context, Func action) => action(); + public ValueTask ExecuteAfterTestSessionHook(MethodMetadata method, TestSessionContext context, Func action) => action(); + public ValueTask ExecuteAfterAssemblyHook(MethodMetadata method, AssemblyHookContext context, Func action) => action(); + public ValueTask ExecuteAfterClassHook(MethodMetadata method, ClassHookContext context, Func action) => action(); + public ValueTask ExecuteAfterTestHook(MethodMetadata method, TestContext context, Func action) => action(); + } + + public sealed class MyFormatter { } + + public sealed class FileLogSink(string path) : TUnit.Core.Logging.ILogSink + { + public string Path { get; } = path; + public bool IsEnabled(TUnit.Core.Logging.LogLevel level) => true; + public void Log(TUnit.Core.Logging.LogLevel level, string message, Exception? exception, Context? context) { } + public ValueTask LogAsync(TUnit.Core.Logging.LogLevel level, string message, Exception? exception, Context? context) => ValueTask.CompletedTask; + } + + public sealed class DebugLogSink : TUnit.Core.Logging.ILogSink + { + public bool IsEnabled(TUnit.Core.Logging.LogLevel level) => true; + public void Log(TUnit.Core.Logging.LogLevel level, string message, Exception? exception, Context? context) { } + public ValueTask LogAsync(TUnit.Core.Logging.LogLevel level, string message, Exception? exception, Context? context) => ValueTask.CompletedTask; + } + + public sealed class DatabaseConnection : IDisposable + { + public ExampleContainer Container { get; } = new(); + public static Task CreateAsync() => Task.FromResult(new DatabaseConnection()); + public void Open() { } + public Task OpenAsync() => Task.CompletedTask; + public void Close() { } + public Task MigrateAsync() => Task.CompletedTask; + public Task ExecuteAsync(string command) => Task.CompletedTask; + public Task SeedAsync() => Task.CompletedTask; + public Task GetUserCountAsync() => Task.FromResult(1); + public Task CountAsync() => Task.FromResult(1); + public Task CreateUserAsync(string name) => Task.FromResult(new User(name)); + public Task CloseAsync() => Task.CompletedTask; + public IEnumerable Query(string query) => DocValues.users; + public void Dispose() { } + } + + public sealed class ExampleContainer + { + public string GetConnectionString() => "Host=localhost;Database=examples"; + } + + public sealed class DatabaseFixture + { + public DatabaseConnection Connection { get; } = new(); + public Task QueryAsync(string query) => Task.FromResult(new()); + } + + public sealed class SomeClass1 { } + public sealed class SomeClass2 { } + public sealed class SomeClass3 { } + public sealed class Class1 { } + public sealed class Class2 { } + public sealed class Class3 { } + public sealed class SomeDependency { } + + public sealed class SomeClass + { + public int One { get; init; } + public int Two { get; init; } + } + + public sealed class Customer { public Address Address { get; init; } = new(); } + public sealed class Order + { + public Guid Id { get; init; } = Guid.NewGuid(); + public int OrderId { get; init; } = 1; + public OrderStatus Status { get; init; } = OrderStatus.Pending; + public int ProductId { get; init; } + public string ProductName { get; init; } = string.Empty; + public decimal Price { get; init; } + public decimal Total { get; init; } + public DateTime? CompletedDate { get; init; } = DateTime.UtcNow; + public OrderItem[] Items { get; init; } = [new("ABC-123")]; + public Customer Customer { get; init; } = new(); + } + public sealed record OrderItem(string Sku); + + public sealed class Product + { + public string Name { get; init; } = string.Empty; + public decimal Price { get; init; } + public string Category { get; init; } = "General"; + public ProductStatus Status { get; init; } = ProductStatus.Active; + public int Stock { get; init; } = 1; + public bool BackorderAllowed { get; init; } + public object? Warranty { get; init; } + public string? ISBN { get; init; } + } + + public sealed class DatabaseContext { } + + public interface ITestDatabase : IDisposable + { + void Initialize(); + Task InitializeAsync(); + } + + public interface IOrderService : IDisposable + { + Order CreateOrder(int productId, string productName, decimal price); + } + + public sealed class OrderService : IOrderService + { + public OrderService(ITestDatabase database) { } + public OrderService(Microsoft.Extensions.Logging.ILogger logger) { } + + public Order CreateOrder(int productId, string productName, decimal price) => + new() { ProductId = productId, ProductName = productName, Price = price }; + + public void ProcessOrder(Order value) { } + + public void Dispose() { } + public static Task CreateAsync() => Task.FromResult(new OrderService(new InMemoryDatabase())); + public static Task CreateAsync(string item) => Task.FromResult(new Order()); + } + + public sealed class ProductService : IDisposable + { + public ProductService(ITestDatabase database) { } + + public Product CreateProduct(string name, decimal price) => new() { Name = name, Price = price }; + public Product? GetProduct(int id) => null; + public void Dispose() { } + } + + public interface IUserRepository { } + public sealed class UserRepository : IUserRepository + { + public UserRepository() { } + public UserRepository(DatabaseConnection connection) { } + public User GetUser(int id) => new() { Id = id }; + public IEnumerable GetAllUsers() => DocValues.users; + public static Task GetByIdAsync(Guid id) => Task.FromResult(DocValues.user); + public static Task CreateAsync(User value) => Task.FromResult(value); + public static Task CreateAsync(string name) => Task.FromResult(new User(name)); + public static Task DeleteAsync(object id) => Task.CompletedTask; + public static Task ExistsAsync(object id) => Task.FromResult(false); + } + + public interface IEmailService { } + public sealed class FakeEmailService : IEmailService + { + public int SentCount { get; private set; } + } + + public sealed class UserService : IService + { + public event Action? OnMessage; + public Microsoft.Extensions.Logging.ILogger? Logger { get; } + public UserService(DatabaseConnection connection) { } + public UserService(IUserRepository repository, IEmailService emailService) { } + public UserService(Microsoft.Extensions.Logging.ILogger logger) => Logger = logger; + public User GetUser(int id) => DocValues.user; + public string GetName() => "Alice"; + public int GetCount() => 1; + public Task InitializeAsync() => Task.CompletedTask; + public Task CreateAsync(string email) => Task.CompletedTask; + public Task CreateUserAsync(string email, string name) => Task.FromResult(new User(name) { Email = email }); + public Task GetUserAsync(int id) => throw new UserNotFoundException(); + public void RaiseMessage(string message) => OnMessage?.Invoke(this, message); + } + + public sealed class UserNotFoundException : Exception { } + + public sealed class ApplicationDbContext : DbContext + { + public DbSet Users => Set(); + } +} + +namespace MyCompany.Testing +{ + public abstract class DatabaseTestBase { } +} diff --git a/tests/TUnit.DocTests/TUnit.DocTests.csproj b/tests/TUnit.DocTests/TUnit.DocTests.csproj index 81a740e6868..8f74624afbb 100644 --- a/tests/TUnit.DocTests/TUnit.DocTests.csproj +++ b/tests/TUnit.DocTests/TUnit.DocTests.csproj @@ -7,29 +7,37 @@ false false false - false - false + true + + false + true false - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -37,18 +45,29 @@ + + + + + + + + + + + diff --git a/tests/TUnit.Engine.Tests/GitHubReporterTests.cs b/tests/TUnit.Engine.Tests/GitHubReporterTests.cs index 38295421c26..d8c31e7d26b 100644 --- a/tests/TUnit.Engine.Tests/GitHubReporterTests.cs +++ b/tests/TUnit.Engine.Tests/GitHubReporterTests.cs @@ -3,6 +3,7 @@ using Shouldly; using TUnit.Engine.Exceptions; using TUnit.Engine.Reporters; +using TUnit.Engine.Reporters.Aggregation; namespace TUnit.Engine.Tests; @@ -114,6 +115,42 @@ public async Task IsEnabledAsync_Should_Return_False_When_GITHUB_ACTIONS_Is_Not_ result.ShouldBeFalse(); } + [Test] + public async Task ClearAggregatedSummary_Removes_Stale_Content() + { + var (reporter, outputFile) = await SetupReporter(); + GitHubSummaryRegion.ReplaceOrAppend(outputFile, "stale aggregate"); + + reporter.ClearAggregatedSummary(); + + File.ReadAllText(outputFile).ShouldNotContain("stale aggregate"); + } + + [Test] + public async Task ResetSessionState_Clears_Test_And_Presentation_State() + { + var (reporter, outputFile) = await SetupReporter(); + await FeedTestMessages(reporter, + CreatePassedTestMessage("retry", "CurrentTest", "Tests"), + CreatePassedTestMessage("retry", "CurrentTest", "Tests"), + CreatePassedTestMessage("stale", "StaleTest", "Tests")); + reporter.ArtifactUrl = "https://example.com/old-artifact"; + reporter.ShowArtifactUploadTip = true; + reporter.SuppressPerSuiteSummary = true; + + reporter.ResetSessionState(); + await FeedTestMessages(reporter, CreatePassedTestMessage("retry", "CurrentTest", "Tests")); + await reporter.AfterRunAsync(0, CancellationToken.None); + + var output = await File.ReadAllTextAsync(outputFile); + output.ShouldContain("**1 tests**"); + output.ShouldNotContain("StaleTest"); + output.ShouldNotContain("flaky"); + reporter.ArtifactUrl.ShouldBeNull(); + reporter.ShowArtifactUploadTip.ShouldBeFalse(); + reporter.SuppressPerSuiteSummary.ShouldBeFalse(); + } + [Test] public async Task AfterRunAsync_Groups_Failures_By_Exception_Type() { diff --git a/tests/TUnit.Engine.Tests/HtmlReporterConfigurationTests.cs b/tests/TUnit.Engine.Tests/HtmlReporterConfigurationTests.cs new file mode 100644 index 00000000000..3b4e53c6d28 --- /dev/null +++ b/tests/TUnit.Engine.Tests/HtmlReporterConfigurationTests.cs @@ -0,0 +1,755 @@ +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.TestHost; +using Shouldly; +using TUnit.Core.Settings; +using TUnit.Engine.Reporters; +using TUnit.Engine.Reporters.Aggregation; +using TUnit.Engine.Reporters.Html; + +namespace TUnit.Engine.Tests; + +[NotInParallel] +public class HtmlReporterConfigurationTests +{ + private bool _htmlReportEnabled; + private bool _jsonReportEnabled; + private bool _artifactUploadEnabled; + private string? _disableHtmlReporter; + private string? _disableJsonReport; + private string? _disableArtifactUpload; + private string? _aggregateReports; + private string? _aggregateDirectory; + + [Before(HookType.Test)] + public void SnapshotConfiguration() + { + _htmlReportEnabled = TUnitSettings.Default.Reporting.HtmlReportEnabled; + _jsonReportEnabled = TUnitSettings.Default.Reporting.JsonReportEnabled; + _artifactUploadEnabled = TUnitSettings.Default.Reporting.ArtifactUploadEnabled; + _disableHtmlReporter = Environment.GetEnvironmentVariable("TUNIT_DISABLE_HTML_REPORTER"); + _disableJsonReport = Environment.GetEnvironmentVariable("TUNIT_DISABLE_JSON_REPORT"); + _disableArtifactUpload = Environment.GetEnvironmentVariable("TUNIT_DISABLE_ARTIFACT_UPLOAD"); + _aggregateReports = Environment.GetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS"); + _aggregateDirectory = Environment.GetEnvironmentVariable("TUNIT_AGGREGATE_DIR"); + + Environment.SetEnvironmentVariable("TUNIT_DISABLE_HTML_REPORTER", null); + Environment.SetEnvironmentVariable("TUNIT_DISABLE_JSON_REPORT", null); + Environment.SetEnvironmentVariable("TUNIT_DISABLE_ARTIFACT_UPLOAD", null); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", null); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", null); + } + + [After(HookType.Test)] + public void RestoreConfiguration() + { + TUnitSettings.Default.Reporting.HtmlReportEnabled = _htmlReportEnabled; + TUnitSettings.Default.Reporting.JsonReportEnabled = _jsonReportEnabled; + TUnitSettings.Default.Reporting.ArtifactUploadEnabled = _artifactUploadEnabled; + Environment.SetEnvironmentVariable("TUNIT_DISABLE_HTML_REPORTER", _disableHtmlReporter); + Environment.SetEnvironmentVariable("TUNIT_DISABLE_JSON_REPORT", _disableJsonReport); + Environment.SetEnvironmentVariable("TUNIT_DISABLE_ARTIFACT_UPLOAD", _disableArtifactUpload); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", _aggregateReports); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", _aggregateDirectory); + } + + [Test] + public async Task Programmatic_Settings_Can_Disable_Html_Reporting_Features() + { + TUnitSettings.Default.Reporting.HtmlReportEnabled = false; + TUnitSettings.Default.Reporting.JsonReportEnabled = false; + TUnitSettings.Default.Reporting.ArtifactUploadEnabled = false; + + await Assert.That(HtmlReporter.IsHtmlReportEnabled()).IsFalse(); + await Assert.That(HtmlReporter.IsJsonReportEnabled()).IsFalse(); + await Assert.That(HtmlReporter.IsArtifactUploadEnabled()).IsFalse(); + } + + [Test] + public async Task Disable_Environment_Variables_Take_Precedence() + { + Environment.SetEnvironmentVariable("TUNIT_DISABLE_HTML_REPORTER", "true"); + Environment.SetEnvironmentVariable("TUNIT_DISABLE_JSON_REPORT", "1"); + Environment.SetEnvironmentVariable("TUNIT_DISABLE_ARTIFACT_UPLOAD", "yes"); + + await Assert.That(HtmlReporter.IsHtmlReportEnabled()).IsFalse(); + await Assert.That(HtmlReporter.IsJsonReportEnabled()).IsFalse(); + await Assert.That(HtmlReporter.IsArtifactUploadEnabled()).IsFalse(); + } + + [Test] + public async Task Disabled_Html_Report_Stops_Activity_Collection_After_Discovery(CancellationToken cancellationToken) + { + using var reporter = new HtmlReporter(new MockExtension()); + cancellationToken.ThrowIfCancellationRequested(); + await reporter.OnTestSessionStartingAsync(null!); + reporter.HasActivityCollector.ShouldBeTrue(); + + TUnitSettings.Default.Reporting.HtmlReportEnabled = false; + await reporter.ConsumeAsync(reporter, null!, cancellationToken); + + reporter.HasActivityCollector.ShouldBeFalse(); + } + + [Test] + public async Task Html_Report_Setting_Is_Resolved_Per_Session(CancellationToken cancellationToken) + { + using var reporter = new HtmlReporter(new MockExtension()); + + cancellationToken.ThrowIfCancellationRequested(); + await reporter.OnTestSessionStartingAsync(null!); + TUnitSettings.Default.Reporting.HtmlReportEnabled = false; + reporter.IsHtmlReportEnabledForRun().ShouldBeFalse(); + + cancellationToken.ThrowIfCancellationRequested(); + await reporter.OnTestSessionStartingAsync(null!); + TUnitSettings.Default.Reporting.HtmlReportEnabled = true; + reporter.IsHtmlReportEnabledForRun().ShouldBeTrue(); + } + + [Test] + public async Task Activity_Collection_Starts_Before_Discovery_Reenables_Reporting(CancellationToken cancellationToken) + { + TUnitSettings.Default.Reporting.HtmlReportEnabled = false; + using var reporter = new HtmlReporter(new MockExtension()); + + cancellationToken.ThrowIfCancellationRequested(); + await reporter.OnTestSessionStartingAsync(null!); + reporter.HasActivityCollector.ShouldBeTrue(); + + var activity = TUnitActivitySource.StartLifecycleActivity(TUnitActivitySource.SpanTestSession); + TUnitSettings.Default.Reporting.HtmlReportEnabled = true; + reporter.IsHtmlReportEnabledForRun().ShouldBeTrue(); + TUnitActivitySource.StopActivity(activity); + + reporter.StopActivityCollection(); + var spans = reporter.BuildReportData().Spans; + spans.ShouldNotBeNull(); + spans.ShouldContain(span => span.SpanType == TUnitActivitySource.SpanTestSession); + } + + [Test] + public async Task Activity_Collection_Is_Recreated_Between_Sessions(CancellationToken cancellationToken) + { + TUnitSettings.Default.Reporting.HtmlReportEnabled = true; + using var reporter = new HtmlReporter(new MockExtension()); + + cancellationToken.ThrowIfCancellationRequested(); + await reporter.OnTestSessionStartingAsync(null!); + reporter.HasActivityCollector.ShouldBeTrue(); + + cancellationToken.ThrowIfCancellationRequested(); + await reporter.OnTestSessionFinishingAsync(null!); + reporter.HasActivityCollector.ShouldBeFalse(); + + cancellationToken.ThrowIfCancellationRequested(); + await reporter.OnTestSessionStartingAsync(null!); + reporter.HasActivityCollector.ShouldBeTrue(); + + var activity = TUnitActivitySource.StartLifecycleActivity(TUnitActivitySource.SpanTestSession); + TUnitActivitySource.StopActivity(activity); + reporter.StopActivityCollection(); + var spans = reporter.BuildReportData().Spans; + spans.ShouldNotBeNull(); + spans.ShouldContain(span => span.SpanType == TUnitActivitySource.SpanTestSession); + } + + [Test] + public async Task Test_Updates_Are_Cleared_Between_Sessions(CancellationToken cancellationToken) + { + using var reporter = new HtmlReporter(new MockExtension()); + + await reporter.OnTestSessionStartingAsync(null!); + await reporter.ConsumeAsync(reporter, CreatePassedUpdate("first"), cancellationToken); + reporter.BuildReportData().Groups.SelectMany(x => x.Tests).Single().Id.ShouldBe("first"); + + await reporter.OnTestSessionStartingAsync(null!); + await reporter.ConsumeAsync(reporter, CreatePassedUpdate("second"), cancellationToken); + + reporter.BuildReportData().Groups.SelectMany(x => x.Tests).Single().Id.ShouldBe("second"); + } + + [Test] + public async Task GitHub_Report_State_Is_Reset_Between_Sessions(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", Path.Combine(tempDirectory, "aggregate")); + + try + { + Directory.CreateDirectory(tempDirectory); + using var reporter = new HtmlReporter(new MockExtension()); + var githubReporter = new GitHubReporter(new MockExtension()); + reporter.SetGitHubReporter(githubReporter); + + await reporter.TryWriteSidecarAndAggregateAsync( + CreateReportData(), + Path.Combine(tempDirectory, "suite-report.html"), + cancellationToken); + githubReporter.SuppressPerSuiteSummary.ShouldBeTrue(); + githubReporter.ArtifactUrl = "https://example.com/old-artifact"; + githubReporter.ShowArtifactUploadTip = true; + + await reporter.OnTestSessionStartingAsync(null!); + + githubReporter.SuppressPerSuiteSummary.ShouldBeFalse(); + githubReporter.ArtifactUrl.ShouldBeNull(); + githubReporter.ShowArtifactUploadTip.ShouldBeFalse(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Disabled_Artifact_Upload_Does_Not_Publish_Session_File_Artifact(CancellationToken cancellationToken) + { + TUnitSettings.Default.Reporting.ArtifactUploadEnabled = false; + var reporter = new HtmlReporter(new MockExtension()); + var messageBus = new CapturingMessageBus(); + reporter.SetMessageBus(messageBus); + + await reporter.PublishArtifactAsync("report.html", new SessionUid("session"), cancellationToken); + + messageBus.Published.ShouldBeEmpty(); + } + + [Test] + public async Task Disabled_Json_Report_Removes_Stale_Aggregation_Outputs(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var removedHtmlPath = Path.Combine(tempDirectory, "removed-report.html"); + var remainingHtmlPath = Path.Combine(tempDirectory, "remaining-report.html"); + var mergedReportPath = Path.Combine(aggregationDirectory, ReportDataJson.MergedReportFileName); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + Directory.CreateDirectory(tempDirectory); + var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + + TUnitSettings.Default.Reporting.JsonReportEnabled = true; + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData("RemovedSuiteMarker"), removedHtmlPath, cancellationToken); + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData("RemainingSuiteMarker"), remainingHtmlPath, cancellationToken); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(2); + + TUnitSettings.Default.Reporting.JsonReportEnabled = false; + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData("RemovedSuiteMarker"), removedHtmlPath, cancellationToken); + + File.Exists(HtmlReporter.GetSidecarPath(removedHtmlPath)).ShouldBeFalse(); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(2); + aggregator.ReadAllSidecars().Single().AssemblyName.ShouldBe("RemainingSuiteMarker"); + var mergedReport = File.ReadAllText(mergedReportPath); + mergedReport.ShouldNotContain("RemovedSuiteMarker"); + mergedReport.ShouldContain("RemainingSuiteMarker"); + + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData("RemainingSuiteMarker"), remainingHtmlPath, cancellationToken); + + File.Exists(HtmlReporter.GetSidecarPath(remainingHtmlPath)).ShouldBeFalse(); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(2); + aggregator.ReadAllSidecars().ShouldBeEmpty(); + File.Exists(mergedReportPath).ShouldBeFalse(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Cancelled_Session_Still_Removes_Disabled_Report_Sidecars(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var htmlPath = Path.Combine(tempDirectory, "cancelled-report.html"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + Directory.CreateDirectory(tempDirectory); + using var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + + TUnitSettings.Default.Reporting.JsonReportEnabled = true; + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData(), htmlPath, cancellationToken); + + using var cancelled = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cancelled.Cancel(); + TUnitSettings.Default.Reporting.JsonReportEnabled = false; + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData(), htmlPath, cancelled.Token); + + File.Exists(HtmlReporter.GetSidecarPath(htmlPath)).ShouldBeFalse(); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(1); + aggregator.ReadAllSidecars().ShouldBeEmpty(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Disabled_Marker_Excludes_Stale_Shared_Sidecar(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", tempDirectory); + + try + { + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + var reportData = CreateReportData("DisabledSuiteMarker"); + aggregator.WriteSidecar(ReportDataJson.SerializeToBytes(reportData), reportData.AssemblyName, "suite"); + var staleGeneration = aggregator.ReadEffectiveSidecarGeneration(reportData.AssemblyName, "suite"); + + var replacement = CreateReportData(reportData.AssemblyName, "replacement-machine"); + aggregator.WriteSidecar(ReportDataJson.SerializeToBytes(replacement), replacement.AssemblyName, "suite"); + aggregator.ExcludeSidecarIfGenerationMatches(reportData.AssemblyName, "suite", staleGeneration); + Directory.GetFiles(tempDirectory, $"*{ReportDataJson.SidecarExclusionExtension}").ShouldBeEmpty(); + aggregator.ReadAllSidecars().Single().MachineName.ShouldBe("replacement-machine"); + + aggregator.ExcludeSidecar(reportData.AssemblyName, "suite"); + aggregator.ReadAllSidecars().ShouldBeEmpty(); + + var latest = CreateReportData(reportData.AssemblyName, "latest-machine"); + aggregator.WriteSidecar(ReportDataJson.SerializeToBytes(latest), latest.AssemblyName, "suite"); + aggregator.ReadAllSidecars().Single().MachineName.ShouldBe("latest-machine"); + + aggregator.IncludeSidecar(reportData.AssemblyName, "suite"); + aggregator.ReadAllSidecars().Single().MachineName.ShouldBe("latest-machine"); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Disabled_Html_Report_Removes_Stale_Aggregation_Outputs(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var disabledHtmlPath = Path.Combine(tempDirectory, "disabled-report.html"); + var remainingHtmlPath = Path.Combine(tempDirectory, "remaining-report.html"); + var mergedReportPath = Path.Combine(aggregationDirectory, ReportDataJson.MergedReportFileName); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + Directory.CreateDirectory(tempDirectory); + using var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + reporter.SetOutputPath(disabledHtmlPath); + var disabledAssemblyName = reporter.BuildReportData().AssemblyName; + + TUnitSettings.Default.Reporting.JsonReportEnabled = true; + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData(disabledAssemblyName), disabledHtmlPath, cancellationToken); + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData("RemainingSuiteMarker"), remainingHtmlPath, cancellationToken); + + TUnitSettings.Default.Reporting.HtmlReportEnabled = false; + await reporter.OnTestSessionFinishingAsync(null!); + + File.Exists(HtmlReporter.GetSidecarPath(disabledHtmlPath)).ShouldBeFalse(); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(2); + aggregator.ReadAllSidecars().Single().AssemblyName.ShouldBe("RemainingSuiteMarker"); + var mergedReport = File.ReadAllText(mergedReportPath); + mergedReport.ShouldNotContain(disabledAssemblyName); + mergedReport.ShouldContain("RemainingSuiteMarker"); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Shared_Sidecar_Stays_Hidden_Until_Lock_Wait_Completes(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var htmlPath = Path.Combine(tempDirectory, "suite-report.html"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + Directory.CreateDirectory(tempDirectory); + using var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable); + var aggregationLock = await aggregator!.AcquireLockAsync(cancellationToken); + aggregationLock.ShouldNotBeNull(); + + Task writeTask; + using (aggregationLock!) + { + writeTask = reporter.TryWriteSidecarAndAggregateAsync(CreateReportData(), htmlPath, cancellationToken); + + File.Exists(HtmlReporter.GetSidecarPath(htmlPath)).ShouldBeTrue(); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(1); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarPublishingExtension}").Length.ShouldBe(1); + aggregator.ReadAllSidecars().ShouldBeEmpty(); + } + + await writeTask; + + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(1); + File.ReadAllText(Path.Combine(aggregationDirectory, ReportDataJson.MergedReportFileName)).ShouldContain("Tests"); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Empty_Session_With_Disabled_Json_Removes_Stale_Sidecars(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var htmlPath = Path.Combine(tempDirectory, "suite-report.html"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + Directory.CreateDirectory(tempDirectory); + using var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + reporter.SetOutputPath(htmlPath); + var assemblyName = reporter.BuildReportData().AssemblyName; + + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData(assemblyName), htmlPath, cancellationToken); + TUnitSettings.Default.Reporting.JsonReportEnabled = false; + + await reporter.OnTestSessionFinishingAsync(null!); + + File.Exists(HtmlReporter.GetSidecarPath(htmlPath)).ShouldBeFalse(); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(1); + aggregator.ReadAllSidecars().ShouldBeEmpty(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Publication_Lock_File_Is_Stable_And_Reused(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", tempDirectory); + + try + { + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + var reportData = CreateReportData(); + using var publicationLock = aggregator.BeginSidecarPublication(reportData.AssemblyName, "suite"); + var sidecarPath = aggregator.WriteSidecar(ReportDataJson.SerializeToBytes(reportData), reportData.AssemblyName, "suite"); + + aggregator.ReadAllSidecars().ShouldBeEmpty(); + publicationLock.Dispose(); + + aggregator.ReadAllSidecars().Single().AssemblyName.ShouldBe("Tests"); + File.Exists(sidecarPath + ReportDataJson.SidecarPublishingExtension).ShouldBeTrue(); + + using (aggregator.BeginSidecarPublication(reportData.AssemblyName, "suite")) + { + aggregator.ReadAllSidecars().ShouldBeEmpty(); + } + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Cancelled_Lock_Wait_Keeps_Per_Suite_Summary(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + using var reporter = new HtmlReporter(new MockExtension()); + var githubReporter = new GitHubReporter(new MockExtension()); + reporter.SetGitHubReporter(githubReporter); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + using var aggregationLock = await aggregator.AcquireLockAsync(cancellationToken); + using var cancelled = new CancellationTokenSource(); + cancelled.Cancel(); + var htmlPath = Path.Combine(tempDirectory, "suite-report.html"); + aggregator.ExcludeSidecar("Tests", htmlPath); + + await reporter.TryWriteSidecarAndAggregateAsync( + CreateReportData(), + htmlPath, + cancelled.Token); + + githubReporter.SuppressPerSuiteSummary.ShouldBeFalse(); + aggregator.ReadAllSidecars().Single().AssemblyName.ShouldBe("Tests"); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExclusionExtension}").ShouldBeEmpty(); + File.Exists(Path.Combine(aggregationDirectory, ReportDataJson.MergedReportFileName)).ShouldBeFalse(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Cancelled_Defer_Lock_Wait_Suppresses_Per_Suite_Summary(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "defer"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + using var reporter = new HtmlReporter(new MockExtension()); + var githubReporter = new GitHubReporter(new MockExtension()); + reporter.SetGitHubReporter(githubReporter); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + using var aggregationLock = await aggregator.AcquireLockAsync(cancellationToken); + using var cancelled = new CancellationTokenSource(); + cancelled.Cancel(); + var htmlPath = Path.Combine(tempDirectory, "suite-report.html"); + + await reporter.TryWriteSidecarAndAggregateAsync( + CreateReportData(), + htmlPath, + cancelled.Token); + + githubReporter.SuppressPerSuiteSummary.ShouldBeTrue(); + aggregator.ReadAllSidecars().Single().AssemblyName.ShouldBe("Tests"); + File.Exists(Path.Combine(aggregationDirectory, ReportDataJson.MergedReportFileName)).ShouldBeFalse(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + [Timeout(30_000)] + public async Task Disabled_Cleanup_Does_Not_Exclude_Active_Publication(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var htmlPath = Path.Combine(tempDirectory, "suite-report.html"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + using var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + var reportData = CreateReportData(); + aggregator.WriteSidecar(ReportDataJson.SerializeToBytes(reportData), reportData.AssemblyName, htmlPath); + TUnitSettings.Default.Reporting.JsonReportEnabled = false; + + using (aggregator.BeginSidecarPublication(reportData.AssemblyName, htmlPath)) + { + var replacement = CreateReportData(reportData.AssemblyName, "active-publisher"); + aggregator.WriteSidecar(ReportDataJson.SerializeToBytes(replacement), replacement.AssemblyName, htmlPath); + await reporter.TryWriteSidecarAndAggregateAsync(reportData, htmlPath, cancellationToken); + } + + aggregator.ReadAllSidecars().Single().MachineName.ShouldBe("active-publisher"); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExclusionExtension}").ShouldBeEmpty(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + [Timeout(30_000)] + public async Task Enabled_Publication_Contention_Preserves_Shared_Sidecar(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var htmlPath = Path.Combine(tempDirectory, "suite-report.html"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + using var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + var reportData = CreateReportData("ContendedSuiteMarker", "timeout-publisher"); + + using (aggregator.BeginSidecarPublication(reportData.AssemblyName, htmlPath)) + { + var activeReport = CreateReportData(reportData.AssemblyName, "active-publisher"); + aggregator.WriteSidecar(ReportDataJson.SerializeToBytes(activeReport), activeReport.AssemblyName, htmlPath); + await reporter.TryWriteSidecarAndAggregateAsync(reportData, htmlPath, cancellationToken); + + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(2); + aggregator.ReadAllSidecars().Single().MachineName.ShouldBe("timeout-publisher"); + File.Exists(Path.Combine(aggregationDirectory, ReportDataJson.MergedReportFileName)).ShouldBeTrue(); + } + + aggregator.ReadAllSidecars().Single().MachineName.ShouldBe("timeout-publisher"); + + var newerReport = CreateReportData(reportData.AssemblyName, "newer-publisher"); + await reporter.TryWriteSidecarAndAggregateAsync(newerReport, htmlPath, cancellationToken); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExtension}").Length.ShouldBe(1); + aggregator.ReadAllSidecars().Single().MachineName.ShouldBe("newer-publisher"); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + [Timeout(30_000)] + public async Task Enabled_Publication_Supersedes_In_Flight_Disabled_Cleanup(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var htmlPath = Path.Combine(tempDirectory, "suite-report.html"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + Directory.CreateDirectory(tempDirectory); + using var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + var oldReport = CreateReportData(machineName: "old-machine"); + await reporter.TryWriteSidecarAndAggregateAsync(oldReport, htmlPath, cancellationToken); + + var aggregationLock = await aggregator.AcquireLockAsync(cancellationToken); + aggregationLock.ShouldNotBeNull(); + Task cleanupTask; + Task publicationTask; + using (aggregationLock!) + { + TUnitSettings.Default.Reporting.JsonReportEnabled = false; + cleanupTask = reporter.TryWriteSidecarAndAggregateAsync(oldReport, htmlPath, cancellationToken); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExclusionExtension}").Length.ShouldBe(1); + + TUnitSettings.Default.Reporting.JsonReportEnabled = true; + var newReport = CreateReportData(machineName: "new-machine"); + publicationTask = reporter.TryWriteSidecarAndAggregateAsync(newReport, htmlPath, cancellationToken); + } + + await Task.WhenAll(cleanupTask, publicationTask); + + var publishedReport = aggregator.ReadAllSidecars().Single(); + publishedReport.MachineName.ShouldBe("new-machine"); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExclusionExtension}").ShouldBeEmpty(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + [Test] + public async Task Enabled_Publication_Clears_Stale_Exclusion(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-test-{Guid.NewGuid():N}"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + var htmlPath = Path.Combine(tempDirectory, "suite-report.html"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true"); + Environment.SetEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory); + + try + { + using var reporter = new HtmlReporter(new MockExtension()); + var aggregator = ReportAggregator.TryCreateFromEnvironment(Environment.GetEnvironmentVariable)!; + aggregator.ExcludeSidecar("Tests", htmlPath); + + await reporter.TryWriteSidecarAndAggregateAsync(CreateReportData(), htmlPath, cancellationToken); + + aggregator.ReadAllSidecars().Single().AssemblyName.ShouldBe("Tests"); + Directory.GetFiles(aggregationDirectory, $"*{ReportDataJson.SidecarExclusionExtension}").ShouldBeEmpty(); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } + + private static ReportData CreateReportData(string assemblyName = "Tests", string machineName = "machine") => new() + { + AssemblyName = assemblyName, + MachineName = machineName, + Timestamp = DateTimeOffset.UtcNow.ToString("O"), + TUnitVersion = "1.0.0", + OperatingSystem = "test", + RuntimeVersion = "test", + Summary = new ReportSummary(), + Groups = [], + }; + + private static TestNodeUpdateMessage CreatePassedUpdate(string id) => new( + new SessionUid("session"), + new TestNode + { + Uid = new TestNodeUid(id), + DisplayName = id, + Properties = new PropertyBag(PassedTestNodeStateProperty.CachedInstance), + }); +} diff --git a/tests/TUnit.Engine.Tests/HtmlReporterTests.cs b/tests/TUnit.Engine.Tests/HtmlReporterTests.cs index 5d7d646d22e..05123ed29cc 100644 --- a/tests/TUnit.Engine.Tests/HtmlReporterTests.cs +++ b/tests/TUnit.Engine.Tests/HtmlReporterTests.cs @@ -30,6 +30,23 @@ public void HtmlReporter_DataTypesProduced_Contains_SessionFileArtifact() producer.DataTypesProduced.ShouldContain(typeof(SessionFileArtifact)); } + [Test] + public async Task Stopping_Activity_Collection_Preserves_Spans_For_Report_Data(CancellationToken cancellationToken) + { + using var reporter = new HtmlReporter(new MockExtension()); + cancellationToken.ThrowIfCancellationRequested(); + await reporter.OnTestSessionStartingAsync(null!); + + var activity = TUnitActivitySource.StartLifecycleActivity(TUnitActivitySource.SpanTestSession); + TUnitActivitySource.StopActivity(activity); + reporter.StopActivityCollection(); + + var reportData = reporter.BuildReportData(); + + reportData.Spans.ShouldNotBeNull(); + reportData.Spans.ShouldContain(span => span.SpanType == TUnitActivitySource.SpanTestSession); + } + [Test] public async Task PublishArtifactAsync_Publishes_SessionFileArtifact_When_SessionContext_Set_And_File_Exists() { diff --git a/tests/TUnit.Engine.Tests/Issue6688Tests.cs b/tests/TUnit.Engine.Tests/Issue6688Tests.cs new file mode 100644 index 00000000000..b1b1294b8ec --- /dev/null +++ b/tests/TUnit.Engine.Tests/Issue6688Tests.cs @@ -0,0 +1,46 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +public class Issue6688Tests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task Timeout_Preserves_Custom_Cancellation_Message() + { + await RunTestsWithFilter( + "/*/*/TimeoutCancellationExceptionTests/Custom_Cancellation_Message", + [ + result => result.ResultSummary.Outcome.ShouldBe("Failed"), + result => result.ResultSummary.Counters.Timeout.ShouldBe(1), + result => result.Results.Single().Output?.ErrorInfo?.Message.ShouldContain("Failed due to XYZ"), + ], + new RunOptions().WithArgument("--detailed-stacktrace")); + } + + [Test] + public async Task Timeout_Preserves_Custom_Non_Cancellation_Exception_Message() + { + await RunTestsWithFilter( + "/*/*/TimeoutCancellationExceptionTests/Custom_Non_Cancellation_Exception_Message", + [ + result => result.ResultSummary.Outcome.ShouldBe("Failed"), + result => result.ResultSummary.Counters.Timeout.ShouldBe(1), + result => result.Results.Single().Output?.ErrorInfo?.Message.ShouldContain("Custom non-cancellation diagnostic"), + ], + new RunOptions().WithArgument("--detailed-stacktrace")); + } + + [Test] + public async Task Timeout_Preserves_Custom_Task_Cancellation_Message() + { + await RunTestsWithFilter( + "/*/*/TimeoutCancellationExceptionTests/Custom_Task_Cancellation_Message", + [ + result => result.ResultSummary.Outcome.ShouldBe("Failed"), + result => result.ResultSummary.Counters.Timeout.ShouldBe(1), + result => result.Results.Single().Output?.ErrorInfo?.Message.ShouldContain("Custom task cancellation diagnostic"), + ], + new RunOptions().WithArgument("--detailed-stacktrace")); + } +} diff --git a/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs b/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs new file mode 100644 index 00000000000..1ca19d4ecfe --- /dev/null +++ b/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs @@ -0,0 +1,45 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +public class ReportingSettingsTests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task Discovery_Hook_Can_Disable_Reporting(CancellationToken cancellationToken) + { + var tempDirectory = Path.Combine(Path.GetTempPath(), $"tunit-report-settings-{Guid.NewGuid():N}"); + var reportPath = Path.Combine(tempDirectory, "report.html"); + var aggregationDirectory = Path.Combine(tempDirectory, "aggregate"); + + try + { + var options = new RunOptions() + .WithArgument("--report-html-filename") + .WithArgument(reportPath) + .WithEnvironmentVariable("TUNIT_DISABLE_HTML_REPORTER", "false") + .WithEnvironmentVariable("TUNIT_DISABLE_JSON_REPORT", "false") + .WithEnvironmentVariable("TUNIT_DISABLE_ARTIFACT_UPLOAD", "false") + .WithEnvironmentVariable("TUNIT_AGGREGATE_REPORTS", "true") + .WithEnvironmentVariable("TUNIT_AGGREGATE_DIR", aggregationDirectory) + .WithEnvironmentVariable("TUNIT_TEST_DISABLE_REPORTING_FROM_DISCOVERY_HOOK", "true") + .WithGracefulCancellationToken(cancellationToken); + + await RunTestsWithFilter( + "/*/*/ReportingSettingsTests/*", + [ + result => result.ResultSummary.Counters.Passed.ShouldBe(1), + _ => File.Exists(reportPath).ShouldBeFalse(), + _ => Directory.Exists(aggregationDirectory).ShouldBeFalse(), + ], + options); + } + finally + { + if (Directory.Exists(tempDirectory)) + { + Directory.Delete(tempDirectory, recursive: true); + } + } + } +} diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index aaef3f7c8ea..a3bc5816772 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -188,6 +188,8 @@ namespace public static . That(.IEnumerable value, [.("value")] string? expression = null) { } public static . That(<.> action, [.("action")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } + [.(1)] + public static . That(? value, [.("value")] string? expression = null) { } [.(2)] public static . That(.StringValue value, [.("value")] string? expression = null) { } [.(3)] @@ -2287,7 +2289,19 @@ namespace .Conditions [.<>("IsValueType", ExpectationMessage="be a value type")] [.<>("IsVisible", CustomName="IsNotVisible", ExpectationMessage="be visible", NegateLogic=true)] [.<>("IsVisible", ExpectationMessage="be visible")] - public static class TypeAssertionExtensions { } + public static class TypeAssertionExtensions + { + [.(ExpectationMessage="be assignable from {sourceType}", InlineMethodBody=true)] + public static . IsAssignableFrom(this value, sourceType) { } + [.(ExpectationMessage="be assignable to {expectedType}", InlineMethodBody=true)] + public static . IsAssignableTo(this value, expectedType) { } + } + public sealed class TypeIsAssignableToAssertion : .<> + { + public TypeIsAssignableToAssertion(.<> context) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public class TypeOfAssertion : . { public TypeOfAssertion(. parentContext) { } @@ -6692,6 +6706,12 @@ namespace .Extensions public static . DoesNotContainGenericParameters(this .<> source) { } public static . IsAbstract(this .<> source) { } public static . IsArray(this .<> source) { } + [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] + public static ._IsAssignableFrom_Type_Assertion IsAssignableFrom(this . source, sourceType, [.("sourceType")] string? sourceTypeExpression = null) + where TActual : { } + [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] + public static ._IsAssignableTo_Type_Assertion IsAssignableTo(this . source, expectedType, [.("expectedType")] string? expectedTypeExpression = null) + where TActual : { } public static . IsByRef(this .<> source) { } public static . IsByRefLike(this .<> source) { } public static . IsCOMObject(this .<> source) { } @@ -6873,6 +6893,18 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public sealed class Type_IsAssignableFrom_Type_Assertion : .<> + { + public Type_IsAssignableFrom_Type_Assertion(.<> context, sourceType) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } + public sealed class Type_IsAssignableTo_Type_Assertion : .<> + { + public Type_IsAssignableTo_Type_Assertion(.<> context, expectedType) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public sealed class UInt16_IsEven_Assertion : . { public UInt16_IsEven_Assertion(. context) { } @@ -7526,6 +7558,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public sealed class TypeValueAssertion : .<> + { + public TypeValueAssertion(? value, string? expression) { } + public new . IsAssignableFrom() { } + public new . IsAssignableTo() { } + public new . IsNotAssignableFrom() { } + public new . IsNotAssignableTo() { } + } public class ValueAssertion : ., . { protected ValueAssertion(. context) { } diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 6320c1887c2..f088c0c775e 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -188,6 +188,7 @@ namespace public static . That(.IEnumerable value, [.("value")] string? expression = null) { } public static . That(<.> action, [.("action")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } + public static . That(? value, [.("value")] string? expression = null) { } public static . That(.StringValue value, [.("value")] string? expression = null) { } public static . That(.? value, [.("value")] string? expression = null) { } public static . That(. value, [.("value")] string? expression = null) { } @@ -2270,7 +2271,19 @@ namespace .Conditions [.<>("IsValueType", ExpectationMessage="be a value type")] [.<>("IsVisible", CustomName="IsNotVisible", ExpectationMessage="be visible", NegateLogic=true)] [.<>("IsVisible", ExpectationMessage="be visible")] - public static class TypeAssertionExtensions { } + public static class TypeAssertionExtensions + { + [.(ExpectationMessage="be assignable from {sourceType}", InlineMethodBody=true)] + public static . IsAssignableFrom(this value, sourceType) { } + [.(ExpectationMessage="be assignable to {expectedType}", InlineMethodBody=true)] + public static . IsAssignableTo(this value, expectedType) { } + } + public sealed class TypeIsAssignableToAssertion : .<> + { + public TypeIsAssignableToAssertion(.<> context) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public class TypeOfAssertion : . { public TypeOfAssertion(. parentContext) { } @@ -6599,6 +6612,12 @@ namespace .Extensions public static . DoesNotContainGenericParameters(this .<> source) { } public static . IsAbstract(this .<> source) { } public static . IsArray(this .<> source) { } + [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] + public static ._IsAssignableFrom_Type_Assertion IsAssignableFrom(this . source, sourceType, [.("sourceType")] string? sourceTypeExpression = null) + where TActual : { } + [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] + public static ._IsAssignableTo_Type_Assertion IsAssignableTo(this . source, expectedType, [.("expectedType")] string? expectedTypeExpression = null) + where TActual : { } public static . IsByRef(this .<> source) { } public static . IsByRefLike(this .<> source) { } public static . IsCOMObject(this .<> source) { } @@ -6780,6 +6799,18 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public sealed class Type_IsAssignableFrom_Type_Assertion : .<> + { + public Type_IsAssignableFrom_Type_Assertion(.<> context, sourceType) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } + public sealed class Type_IsAssignableTo_Type_Assertion : .<> + { + public Type_IsAssignableTo_Type_Assertion(.<> context, expectedType) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public sealed class UInt16_IsEven_Assertion : . { public UInt16_IsEven_Assertion(. context) { } @@ -7432,6 +7463,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public sealed class TypeValueAssertion : .<> + { + public TypeValueAssertion(? value, string? expression) { } + public new . IsAssignableFrom() { } + public new . IsAssignableTo() { } + public new . IsNotAssignableFrom() { } + public new . IsNotAssignableTo() { } + } public class ValueAssertion : ., . { protected ValueAssertion(. context) { } diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index bad033c441a..3af6e98b1a7 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -188,6 +188,8 @@ namespace public static . That(.IEnumerable value, [.("value")] string? expression = null) { } public static . That(<.> action, [.("action")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } + [.(1)] + public static . That(? value, [.("value")] string? expression = null) { } [.(2)] public static . That(.StringValue value, [.("value")] string? expression = null) { } [.(3)] @@ -2287,7 +2289,19 @@ namespace .Conditions [.<>("IsValueType", ExpectationMessage="be a value type")] [.<>("IsVisible", CustomName="IsNotVisible", ExpectationMessage="be visible", NegateLogic=true)] [.<>("IsVisible", ExpectationMessage="be visible")] - public static class TypeAssertionExtensions { } + public static class TypeAssertionExtensions + { + [.(ExpectationMessage="be assignable from {sourceType}", InlineMethodBody=true)] + public static . IsAssignableFrom(this value, sourceType) { } + [.(ExpectationMessage="be assignable to {expectedType}", InlineMethodBody=true)] + public static . IsAssignableTo(this value, expectedType) { } + } + public sealed class TypeIsAssignableToAssertion : .<> + { + public TypeIsAssignableToAssertion(.<> context) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public class TypeOfAssertion : . { public TypeOfAssertion(. parentContext) { } @@ -6692,6 +6706,12 @@ namespace .Extensions public static . DoesNotContainGenericParameters(this .<> source) { } public static . IsAbstract(this .<> source) { } public static . IsArray(this .<> source) { } + [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] + public static ._IsAssignableFrom_Type_Assertion IsAssignableFrom(this . source, sourceType, [.("sourceType")] string? sourceTypeExpression = null) + where TActual : { } + [.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")] + public static ._IsAssignableTo_Type_Assertion IsAssignableTo(this . source, expectedType, [.("expectedType")] string? expectedTypeExpression = null) + where TActual : { } public static . IsByRef(this .<> source) { } public static . IsByRefLike(this .<> source) { } public static . IsCOMObject(this .<> source) { } @@ -6873,6 +6893,18 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public sealed class Type_IsAssignableFrom_Type_Assertion : .<> + { + public Type_IsAssignableFrom_Type_Assertion(.<> context, sourceType) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } + public sealed class Type_IsAssignableTo_Type_Assertion : .<> + { + public Type_IsAssignableTo_Type_Assertion(.<> context, expectedType) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public sealed class UInt16_IsEven_Assertion : . { public UInt16_IsEven_Assertion(. context) { } @@ -7526,6 +7558,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public sealed class TypeValueAssertion : .<> + { + public TypeValueAssertion(? value, string? expression) { } + public new . IsAssignableFrom() { } + public new . IsAssignableTo() { } + public new . IsNotAssignableFrom() { } + public new . IsNotAssignableTo() { } + } public class ValueAssertion : ., . { protected ValueAssertion(. context) { } diff --git a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index 8e38d658c79..5091dcc607d 100644 --- a/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -151,6 +151,7 @@ namespace public static . That(.IEnumerable value, [.("value")] string? expression = null) { } public static . That(<.> action, [.("action")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } + public static . That(? value, [.("value")] string? expression = null) { } public static . That(.StringValue value, [.("value")] string? expression = null) { } public static . That(.? value, [.("value")] string? expression = null) { } public static . That(.? value, [.("value")] string? expression = null) { } @@ -2039,7 +2040,19 @@ namespace .Conditions [.<>("IsValueType", ExpectationMessage="be a value type")] [.<>("IsVisible", CustomName="IsNotVisible", ExpectationMessage="be visible", NegateLogic=true)] [.<>("IsVisible", ExpectationMessage="be visible")] - public static class TypeAssertionExtensions { } + public static class TypeAssertionExtensions + { + [.(ExpectationMessage="be assignable from {sourceType}", InlineMethodBody=true)] + public static . IsAssignableFrom(this value, sourceType) { } + [.(ExpectationMessage="be assignable to {expectedType}", InlineMethodBody=true)] + public static . IsAssignableTo(this value, expectedType) { } + } + public sealed class TypeIsAssignableToAssertion : .<> + { + public TypeIsAssignableToAssertion(.<> context) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public class TypeOfAssertion : . { public TypeOfAssertion(. parentContext) { } @@ -5722,6 +5735,10 @@ namespace .Extensions public static . DoesNotContainGenericParameters(this .<> source) { } public static . IsAbstract(this .<> source) { } public static . IsArray(this .<> source) { } + public static ._IsAssignableFrom_Type_Assertion IsAssignableFrom(this . source, sourceType, [.("sourceType")] string? sourceTypeExpression = null) + where TActual : { } + public static ._IsAssignableTo_Type_Assertion IsAssignableTo(this . source, expectedType, [.("expectedType")] string? expectedTypeExpression = null) + where TActual : { } public static . IsByRef(this .<> source) { } public static . IsCOMObject(this .<> source) { } public static . IsClass(this .<> source) { } @@ -5903,6 +5920,18 @@ namespace .Extensions protected override .<.> CheckAsync(.<> metadata) { } protected override string GetExpectation() { } } + public sealed class Type_IsAssignableFrom_Type_Assertion : .<> + { + public Type_IsAssignableFrom_Type_Assertion(.<> context, sourceType) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } + public sealed class Type_IsAssignableTo_Type_Assertion : .<> + { + public Type_IsAssignableTo_Type_Assertion(.<> context, expectedType) { } + protected override .<.> CheckAsync(.<> metadata) { } + protected override string GetExpectation() { } + } public sealed class UInt16_IsEven_Assertion : . { public UInt16_IsEven_Assertion(. context) { } @@ -6486,6 +6515,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public sealed class TypeValueAssertion : .<> + { + public TypeValueAssertion(? value, string? expression) { } + public new . IsAssignableFrom() { } + public new . IsAssignableTo() { } + public new . IsNotAssignableFrom() { } + public new . IsNotAssignableTo() { } + } public class ValueAssertion : ., . { protected ValueAssertion(. context) { } diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 863682c5d02..2c000ddceac 100644 --- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -3046,11 +3046,18 @@ namespace .Settings { public int? MaximumParallelTests { get; set; } } + public sealed class ReportingSettings + { + public bool ArtifactUploadEnabled { get; set; } + public bool HtmlReportEnabled { get; set; } + public bool JsonReportEnabled { get; set; } + } public sealed class TUnitSettings { public . Display { get; } public . Execution { get; } public . Parallelism { get; } + public . Reporting { get; } public . Timeouts { get; } } public sealed class TimeoutSettings diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index df987e2c496..7829d687fc6 100644 --- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -3046,11 +3046,18 @@ namespace .Settings { public int? MaximumParallelTests { get; set; } } + public sealed class ReportingSettings + { + public bool ArtifactUploadEnabled { get; set; } + public bool HtmlReportEnabled { get; set; } + public bool JsonReportEnabled { get; set; } + } public sealed class TUnitSettings { public . Display { get; } public . Execution { get; } public . Parallelism { get; } + public . Reporting { get; } public . Timeouts { get; } } public sealed class TimeoutSettings diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index 8657b96496c..11d504d7622 100644 --- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -3046,11 +3046,18 @@ namespace .Settings { public int? MaximumParallelTests { get; set; } } + public sealed class ReportingSettings + { + public bool ArtifactUploadEnabled { get; set; } + public bool HtmlReportEnabled { get; set; } + public bool JsonReportEnabled { get; set; } + } public sealed class TUnitSettings { public . Display { get; } public . Execution { get; } public . Parallelism { get; } + public . Reporting { get; } public . Timeouts { get; } } public sealed class TimeoutSettings diff --git a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt index bc6b0a77b30..fb14bdc8ccc 100644 --- a/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/tests/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -2968,11 +2968,18 @@ namespace .Settings { public int? MaximumParallelTests { get; set; } } + public sealed class ReportingSettings + { + public bool ArtifactUploadEnabled { get; set; } + public bool HtmlReportEnabled { get; set; } + public bool JsonReportEnabled { get; set; } + } public sealed class TUnitSettings { public . Display { get; } public . Execution { get; } public . Parallelism { get; } + public . Reporting { get; } public . Timeouts { get; } } public sealed class TimeoutSettings diff --git a/tests/TUnit.TestProject/Bugs/_6688/TimeoutCancellationExceptionTests.cs b/tests/TUnit.TestProject/Bugs/_6688/TimeoutCancellationExceptionTests.cs new file mode 100644 index 00000000000..76e10e33d31 --- /dev/null +++ b/tests/TUnit.TestProject/Bugs/_6688/TimeoutCancellationExceptionTests.cs @@ -0,0 +1,58 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._6688; + +public class TimeoutCancellationExceptionTests +{ + [Test] + [Timeout(50)] + [EngineTest(ExpectedResult.Failure)] + public async Task Custom_Cancellation_Message(CancellationToken cancellationToken) + { + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException ex) + { + // Simulate Aspire gathering resource diagnostics after cancellation. + await Task.Delay(50); + throw new OperationCanceledException("Failed due to XYZ", ex.CancellationToken); + } + } + + [Test] + [Timeout(50)] + [EngineTest(ExpectedResult.Failure)] + public async Task Custom_Non_Cancellation_Exception_Message(CancellationToken cancellationToken) + { + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException) + { + await Task.Delay(50); + throw new InvalidOperationException("Custom non-cancellation diagnostic"); + } + } + + [Test] + [Timeout(50)] + [EngineTest(ExpectedResult.Failure)] + public async Task Custom_Task_Cancellation_Message(CancellationToken cancellationToken) + { + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException ex) + { + await Task.Delay(50); + throw new TaskCanceledException( + "Custom task cancellation diagnostic", + new InvalidOperationException("Inner diagnostic"), + ex.CancellationToken); + } + } +} diff --git a/tests/TUnit.TestProject/ReportingSettingsTests.cs b/tests/TUnit.TestProject/ReportingSettingsTests.cs new file mode 100644 index 00000000000..5bba1a327da --- /dev/null +++ b/tests/TUnit.TestProject/ReportingSettingsTests.cs @@ -0,0 +1,28 @@ +namespace TUnit.TestProject; + +public static class ReportingSettingsHooks +{ + internal const string DisableReportingEnvironmentVariable = "TUNIT_TEST_DISABLE_REPORTING_FROM_DISCOVERY_HOOK"; + + [Before(TestDiscovery)] + public static void ConfigureReporting(BeforeTestDiscoveryContext context, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (Environment.GetEnvironmentVariable(DisableReportingEnvironmentVariable) is not "true") + { + return; + } + + context.Settings.Reporting.HtmlReportEnabled = false; + context.Settings.Reporting.JsonReportEnabled = false; + context.Settings.Reporting.ArtifactUploadEnabled = false; + } +} + +public class ReportingSettingsTests +{ + [Test] + public void Test(CancellationToken cancellationToken) + => cancellationToken.ThrowIfCancellationRequested(); +} diff --git a/tests/TUnit.UnitTests/ReportAggregationTests.cs b/tests/TUnit.UnitTests/ReportAggregationTests.cs index c739508d64c..dd5a38bf963 100644 --- a/tests/TUnit.UnitTests/ReportAggregationTests.cs +++ b/tests/TUnit.UnitTests/ReportAggregationTests.cs @@ -17,6 +17,7 @@ public async Task Serialize_Then_Deserialize_RoundTrips_AllFields() await Assert.That(restored).IsNotNull(); await Assert.That(restored!.AssemblyName).IsEqualTo("My.Tests"); + await Assert.That(restored.PublicationGeneration).IsNotNull(); await Assert.That(restored.MachineName).IsEqualTo(original.MachineName); await Assert.That(restored.Timestamp).IsEqualTo(original.Timestamp); await Assert.That(restored.TUnitVersion).IsEqualTo(original.TUnitVersion); diff --git a/tests/TUnit.UnitTests/TUnitSettingsTests.cs b/tests/TUnit.UnitTests/TUnitSettingsTests.cs index 287fdbd63b5..054ba8ff403 100644 --- a/tests/TUnit.UnitTests/TUnitSettingsTests.cs +++ b/tests/TUnit.UnitTests/TUnitSettingsTests.cs @@ -18,6 +18,9 @@ public class TUnitSettingsTests private int? _savedMaximumParallelTests; private bool _savedDetailedStackTrace; private bool _savedFailFast; + private bool _savedHtmlReportEnabled; + private bool _savedJsonReportEnabled; + private bool _savedArtifactUploadEnabled; [Before(HookType.Test)] public void SnapshotSettings() @@ -29,6 +32,9 @@ public void SnapshotSettings() _savedMaximumParallelTests = TUnitSettings.Default.Parallelism.MaximumParallelTests; _savedDetailedStackTrace = TUnitSettings.Default.Display.DetailedStackTrace; _savedFailFast = TUnitSettings.Default.Execution.FailFast; + _savedHtmlReportEnabled = TUnitSettings.Default.Reporting.HtmlReportEnabled; + _savedJsonReportEnabled = TUnitSettings.Default.Reporting.JsonReportEnabled; + _savedArtifactUploadEnabled = TUnitSettings.Default.Reporting.ArtifactUploadEnabled; } [After(HookType.Test)] @@ -41,6 +47,9 @@ public void RestoreSettings() TUnitSettings.Default.Parallelism.MaximumParallelTests = _savedMaximumParallelTests; TUnitSettings.Default.Display.DetailedStackTrace = _savedDetailedStackTrace; TUnitSettings.Default.Execution.FailFast = _savedFailFast; + TUnitSettings.Default.Reporting.HtmlReportEnabled = _savedHtmlReportEnabled; + TUnitSettings.Default.Reporting.JsonReportEnabled = _savedJsonReportEnabled; + TUnitSettings.Default.Reporting.ArtifactUploadEnabled = _savedArtifactUploadEnabled; } [Test] @@ -53,6 +62,9 @@ public async Task Defaults_Are_Correct() await Assert.That(TUnitSettings.Default.Parallelism.MaximumParallelTests).IsNull(); await Assert.That(TUnitSettings.Default.Display.DetailedStackTrace).IsFalse(); await Assert.That(TUnitSettings.Default.Execution.FailFast).IsFalse(); + await Assert.That(TUnitSettings.Default.Reporting.HtmlReportEnabled).IsTrue(); + await Assert.That(TUnitSettings.Default.Reporting.JsonReportEnabled).IsTrue(); + await Assert.That(TUnitSettings.Default.Reporting.ArtifactUploadEnabled).IsTrue(); } [Test] @@ -62,6 +74,18 @@ public async Task Settings_Can_Be_Modified() await Assert.That(TUnitSettings.Default.Timeouts.DefaultTestTimeout).IsEqualTo(TimeSpan.FromMinutes(10)); } + [Test] + public async Task Reporting_Settings_Can_Be_Modified() + { + TUnitSettings.Default.Reporting.HtmlReportEnabled = false; + TUnitSettings.Default.Reporting.JsonReportEnabled = false; + TUnitSettings.Default.Reporting.ArtifactUploadEnabled = false; + + await Assert.That(TUnitSettings.Default.Reporting.HtmlReportEnabled).IsFalse(); + await Assert.That(TUnitSettings.Default.Reporting.JsonReportEnabled).IsFalse(); + await Assert.That(TUnitSettings.Default.Reporting.ArtifactUploadEnabled).IsFalse(); + } + // Covers TestCoordinator's `test.Timeout ?? TUnitSettings...ExplicitDefaultTestTimeout` fallback: // when the user never assigns DefaultTestTimeout, tests without [Timeout] skip the // TimeoutHelper wrapper entirely (the right-hand side of the coalesce is null). diff --git a/tests/TUnit.UnitTests/TimeoutHelperTests.cs b/tests/TUnit.UnitTests/TimeoutHelperTests.cs new file mode 100644 index 00000000000..25d175e1f66 --- /dev/null +++ b/tests/TUnit.UnitTests/TimeoutHelperTests.cs @@ -0,0 +1,89 @@ +using TUnit.Engine.Helpers; + +namespace TUnit.UnitTests; + +public class TimeoutHelperTests +{ + [Test] + public async Task Timeout_Preserves_Exception_Thrown_During_Cancellation() + { + const string cancellationMessage = "Failed due to XYZ"; + + var exception = await Assert.That(async () => + await TimeoutHelper.ExecuteWithTimeoutAsync( + async cancellationToken => + { + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException ex) + { + // Keep execution incomplete long enough for timeout detection to win before + // cancellation diagnostics finish, matching the Aspire failure in #6688. + await Task.Delay(50); + throw new OperationCanceledException(cancellationMessage, ex.CancellationToken); + } + }, + TimeSpan.FromMilliseconds(50), + CancellationToken.None)) + .ThrowsExactly(); + + await Assert.That(exception!.Message).Contains(cancellationMessage); + await Assert.That(exception.InnerException).IsTypeOf(); + await Assert.That(exception.InnerException!.Message).IsEqualTo(cancellationMessage); + } + + [Test] + public async Task Timeout_Does_Not_Preserve_Routine_Operation_Cancellation() + { + var exception = await Assert.That(async () => + await TimeoutHelper.ExecuteWithTimeoutAsync( + async cancellationToken => + { + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException) + { + cancellationToken.ThrowIfCancellationRequested(); + } + }, + TimeSpan.FromMilliseconds(50), + CancellationToken.None)) + .ThrowsExactly(); + + await Assert.That(exception!.InnerException).IsNull(); + await Assert.That(exception.Message).DoesNotContain(nameof(OperationCanceledException)); + } + + [Test] + public async Task Timeout_Preserves_Custom_Task_Cancellation() + { + const string cancellationMessage = "Custom task cancellation diagnostic"; + var diagnosticException = new InvalidOperationException("Inner diagnostic"); + + var exception = await Assert.That(async () => + await TimeoutHelper.ExecuteWithTimeoutAsync( + async cancellationToken => + { + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + } + catch (OperationCanceledException) + { + await Task.Delay(50); + throw new TaskCanceledException(cancellationMessage, diagnosticException, cancellationToken); + } + }, + TimeSpan.FromMilliseconds(50), + CancellationToken.None)) + .ThrowsExactly(); + + var taskCanceledException = await Assert.That(exception!.InnerException).IsTypeOf(); + await Assert.That(taskCanceledException!.Message).IsEqualTo(cancellationMessage); + await Assert.That(taskCanceledException.InnerException).IsSameReferenceAs(diagnosticException); + } +} diff --git a/tools/TUnit.DocSnippetGenerator/Program.cs b/tools/TUnit.DocSnippetGenerator/Program.cs index 564e23e4593..3574c43f47b 100644 --- a/tools/TUnit.DocSnippetGenerator/Program.cs +++ b/tools/TUnit.DocSnippetGenerator/Program.cs @@ -24,11 +24,6 @@ var snippets = new List(); var documentedPackages = new HashSet(StringComparer.OrdinalIgnoreCase); -var contextualSnippets = 0; -var contextualFiles = new HashSet(StringComparer.Ordinal); -var excludedSnippets = 0; -var excludedFiles = 0; - foreach (var document in documents) { ReadDocument(document); @@ -39,18 +34,57 @@ { File.Delete(oldFile); } +var isolatedDirectory = Path.Combine(outputDirectory, "isolated"); +if (Directory.Exists(isolatedDirectory)) +{ + Directory.Delete(isolatedDirectory, recursive: true); +} -var emittedAssemblyAttributes = new HashSet(StringComparer.Ordinal); +var isolatedSharedDocuments = snippets + .Where(snippet => snippet.SharedDocumentId is not null && RequiresIsolatedCompilation(snippet.Prelude + snippet.Source)) + .Select(snippet => snippet.SharedDocumentId!) + .ToHashSet(StringComparer.Ordinal); +var sharedSources = snippets + .Where(snippet => snippet.SharedDocumentId is not null) + .GroupBy(snippet => snippet.SharedDocumentId!, StringComparer.Ordinal) + .ToDictionary( + group => group.Key, + group => string.Join('\n', group.Select(snippet => snippet.Source)), + StringComparer.Ordinal); +var emittedAssemblyAttributes = new Dictionary>(StringComparer.OrdinalIgnoreCase); +var isolatedSnippets = 0; for (var index = 0; index < snippets.Count; index++) { + var snippet = snippets[index]; + var requiresIsolation = RequiresIsolatedCompilation(snippet.Prelude + snippet.Source) || + snippet.SharedDocumentId is not null && isolatedSharedDocuments.Contains(snippet.SharedDocumentId); + var isolatedGroupName = snippet.SharedDocumentId is null ? $"Snippet{index}" : $"Shared_{snippet.SharedDocumentId}"; + var snippetOutputDirectory = requiresIsolation + ? Path.Combine(isolatedDirectory, isolatedGroupName) + : outputDirectory; + if (snippetOutputDirectory != outputDirectory) + { + isolatedSnippets++; + Directory.CreateDirectory(snippetOutputDirectory); + } + + if (!emittedAssemblyAttributes.TryGetValue(snippetOutputDirectory, out var assemblyAttributes)) + { + assemblyAttributes = new HashSet(StringComparer.Ordinal); + emittedAssemblyAttributes[snippetOutputDirectory] = assemblyAttributes; + } + File.WriteAllText( - Path.Combine(outputDirectory, $"Snippet{index}.g.cs"), - GenerateSource(snippets[index], index, emittedAssemblyAttributes), + Path.Combine(snippetOutputDirectory, $"Snippet{index}.g.cs"), + GenerateSource( + snippet, + index, + assemblyAttributes, + snippet.SharedDocumentId is not null ? sharedSources[snippet.SharedDocumentId] : snippet.Source), new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } -Console.WriteLine($"Generated {snippets.Count} C# documentation snippets."); -Console.WriteLine($"Skipped {contextualSnippets} explicitly contextual snippets declared by {contextualFiles.Count} files, {excludedSnippets} explicit snippets, and {excludedFiles} files."); +Console.WriteLine($"Generated {snippets.Count} C# documentation snippets ({isolatedSnippets} require isolated compilation). "); Console.WriteLine($"Documented TUnit packages: {string.Join(", ", documentedPackages.Order())}"); return 0; @@ -58,28 +92,21 @@ void ReadDocument(string documentPath) { var relativePath = Path.GetRelativePath(repositoryRoot, documentPath).Replace('\\', '/'); var lines = File.ReadAllLines(documentPath); + var sharedDocumentId = lines.Any(line => line.Trim() == "") + ? Regex.Replace(relativePath, "[^A-Za-z0-9_]", "_") + : null; ReadDocumentedPackages(lines); - var fileIgnoreDirective = lines - .Select(line => Regex.Match(line.Trim(), "^$")) - .FirstOrDefault(match => match.Success); - if (fileIgnoreDirective?.Success == true) - { - excludedFiles++; - Console.WriteLine($"EXCLUDED {relativePath} ({fileIgnoreDirective.Groups[1].Value})"); - return; - } - - var contextualFileDirective = lines - .Select(line => Regex.Match(line.Trim(), "^$")) - .FirstOrDefault(match => match.Success); - if (lines.Any(line => line.Trim().StartsWith("$"); - if (directive.StartsWith("$"); + var splitMode = Regex.Match(directive, "^$"); + if (directive.StartsWith("$"); - var splitMode = Regex.Match(directive, "^$"); - var contextualMode = Regex.IsMatch(directive, "^$"); - if (directive.StartsWith("