diff --git a/TUnit.Assertions.Tests/CollectionAssertionTests.cs b/TUnit.Assertions.Tests/CollectionAssertionTests.cs new file mode 100644 index 0000000000..1cc925828e --- /dev/null +++ b/TUnit.Assertions.Tests/CollectionAssertionTests.cs @@ -0,0 +1,36 @@ +namespace TUnit.Assertions.Tests; + +public class CollectionAssertionTests +{ + [Test] + public async Task IsEmpty() + { + var items = new List(); + + await Assert.That(items).IsEmpty(); + } + + [Test] + public async Task IsEmpty2() + { + var items = new List(); + + await Assert.That(() => items).IsEmpty(); + } + + [Test] + public async Task Count() + { + var items = new List(); + + await Assert.That(items).Count().IsEqualTo(0); + } + + [Test] + public async Task Count2() + { + var items = new List(); + + await Assert.That(() => items).Count().IsEqualTo(0); + } +} diff --git a/TUnit.Assertions.Tests/FuncCollectionAssertionTests.cs b/TUnit.Assertions.Tests/FuncCollectionAssertionTests.cs new file mode 100644 index 0000000000..ffa4fee039 --- /dev/null +++ b/TUnit.Assertions.Tests/FuncCollectionAssertionTests.cs @@ -0,0 +1,229 @@ +using TUnit.Assertions.Exceptions; + +namespace TUnit.Assertions.Tests; + +/// +/// Tests for FuncCollectionAssertion - verifies that collection assertions work when +/// collections are wrapped in lambdas. Addresses GitHub issue #3910. +/// +public class FuncCollectionAssertionTests +{ + [Test] + public async Task Lambda_Collection_IsEmpty_Passes_For_Empty_Collection() + { + var items = new List(); + await Assert.That(() => items).IsEmpty(); + } + + [Test] + public async Task Lambda_Collection_IsEmpty_Fails_For_NonEmpty_Collection() + { + var items = new List { 1, 2, 3 }; + await Assert.That(async () => await Assert.That(() => items).IsEmpty()) + .Throws(); + } + + [Test] + public async Task Lambda_Collection_IsNotEmpty_Passes_For_NonEmpty_Collection() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).IsNotEmpty(); + } + + [Test] + public async Task Lambda_Collection_IsNotEmpty_Fails_For_Empty_Collection() + { + var items = new List(); + await Assert.That(async () => await Assert.That(() => items).IsNotEmpty()) + .Throws(); + } + + [Test] + public async Task Lambda_Collection_Count_IsEqualTo_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).Count().IsEqualTo(3); + } + + [Test] + public async Task Lambda_Collection_Count_IsGreaterThan_Passes() + { + var items = new List { 1, 2, 3, 4, 5 }; + await Assert.That(() => items).Count().IsGreaterThan(3); + } + + [Test] + public async Task Lambda_Collection_Contains_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).Contains(2); + } + + [Test] + public async Task Lambda_Collection_Contains_Fails_When_Item_Not_Present() + { + var items = new List { 1, 2, 3 }; + await Assert.That(async () => await Assert.That(() => items).Contains(99)) + .Throws(); + } + + [Test] + public async Task Lambda_Collection_DoesNotContain_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).DoesNotContain(99); + } + + [Test] + public async Task Lambda_Collection_HasSingleItem_Passes() + { + var items = new List { 42 }; + await Assert.That(() => items).HasSingleItem(); + } + + [Test] + public async Task Lambda_Collection_All_Passes() + { + var items = new List { 2, 4, 6 }; + await Assert.That(() => items).All(x => x % 2 == 0); + } + + [Test] + public async Task Lambda_Collection_Any_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).Any(x => x > 2); + } + + [Test] + public async Task Lambda_Collection_IsInOrder_Passes() + { + var items = new List { 1, 2, 3, 4, 5 }; + await Assert.That(() => items).IsInOrder(); + } + + [Test] + public async Task Lambda_Collection_IsInDescendingOrder_Passes() + { + var items = new List { 5, 4, 3, 2, 1 }; + await Assert.That(() => items).IsInDescendingOrder(); + } + + [Test] + public async Task Lambda_Collection_HasDistinctItems_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).HasDistinctItems(); + } + + [Test] + public async Task Lambda_Collection_Throws_Passes_When_Exception_Thrown() + { + await Assert.That(() => ThrowingMethod()).Throws(); + } + + [Test] + public async Task Lambda_Collection_Chaining_With_And() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items) + .IsNotEmpty() + .And.Contains(2) + .And.HasDistinctItems(); + } + + [Test] + public async Task Lambda_Array_IsEmpty_Passes() + { + var items = Array.Empty(); + await Assert.That(() => items).IsEmpty(); + } + + [Test] + public async Task Lambda_Array_IsNotEmpty_Passes() + { + var items = new[] { "a", "b", "c" }; + await Assert.That(() => items).IsNotEmpty(); + } + + [Test] + public async Task Lambda_Enumerable_IsEmpty_Passes() + { + IEnumerable items = Enumerable.Empty(); + await Assert.That(() => items).IsEmpty(); + } + + [Test] + public async Task Lambda_HashSet_Contains_Passes() + { + var items = new HashSet { 1, 2, 3 }; + await Assert.That(() => items).Contains(2); + } + + private static IEnumerable ThrowingMethod() + { + throw new InvalidOperationException("Test exception"); + } + + // Async lambda tests + [Test] + public async Task AsyncLambda_Collection_IsEmpty_Passes() + { + await Assert.That(async () => await GetEmptyCollectionAsync()).IsEmpty(); + } + + [Test] + public async Task AsyncLambda_Collection_IsNotEmpty_Passes() + { + await Assert.That(async () => await GetCollectionAsync()).IsNotEmpty(); + } + + [Test] + public async Task AsyncLambda_Collection_Count_IsEqualTo_Passes() + { + await Assert.That(async () => await GetCollectionAsync()).Count().IsEqualTo(3); + } + + [Test] + public async Task AsyncLambda_Collection_Contains_Passes() + { + await Assert.That(async () => await GetCollectionAsync()).Contains(2); + } + + [Test] + public async Task AsyncLambda_Collection_All_Passes() + { + await Assert.That(async () => await GetCollectionAsync()).All(x => x > 0); + } + + [Test] + public async Task AsyncLambda_Collection_Throws_Passes() + { + await Assert.That(async () => await ThrowingMethodAsync()).Throws(); + } + + [Test] + public async Task AsyncLambda_Collection_Chaining_With_And() + { + await Assert.That(async () => await GetCollectionAsync()) + .IsNotEmpty() + .And.Contains(2) + .And.HasDistinctItems(); + } + + private static Task> GetEmptyCollectionAsync() + { + return Task.FromResult>(new List()); + } + + private static Task> GetCollectionAsync() + { + return Task.FromResult>(new List { 1, 2, 3 }); + } + + private static async Task> ThrowingMethodAsync() + { + await Task.Yield(); + throw new InvalidOperationException("Test exception"); + } +} diff --git a/TUnit.Assertions/Extensions/Assert.cs b/TUnit.Assertions/Extensions/Assert.cs index 2e4a731f2e..c1835304b7 100644 --- a/TUnit.Assertions/Extensions/Assert.cs +++ b/TUnit.Assertions/Extensions/Assert.cs @@ -86,6 +86,20 @@ public static ValueAssertion That( return new ValueAssertion(value, expression); } + /// + /// Creates an assertion for a synchronous function that returns a collection. + /// This overload enables collection-specific assertions (IsEmpty, IsNotEmpty, HasCount, etc.) on lambda-wrapped collections. + /// Example: await Assert.That(() => GetItems()).IsEmpty(); + /// Example: await Assert.That(() => GetItems()).HasCount(5); + /// + [OverloadResolutionPriority(1)] + public static FuncCollectionAssertion That( + Func?> func, + [CallerArgumentExpression(nameof(func))] string? expression = null) + { + return new FuncCollectionAssertion(func!, expression); + } + /// /// Creates an assertion for a synchronous function. /// Example: await Assert.That(() => GetValue()).IsGreaterThan(10); @@ -97,6 +111,20 @@ public static FuncAssertion That( return new FuncAssertion(func, expression); } + /// + /// Creates an assertion for an asynchronous function that returns a collection. + /// This overload enables collection-specific assertions (IsEmpty, IsNotEmpty, HasCount, etc.) on async lambda-wrapped collections. + /// Example: await Assert.That(async () => await GetItemsAsync()).IsEmpty(); + /// Example: await Assert.That(async () => await GetItemsAsync()).HasCount(5); + /// + [OverloadResolutionPriority(1)] + public static AsyncFuncCollectionAssertion That( + Func?>> func, + [CallerArgumentExpression(nameof(func))] string? expression = null) + { + return new AsyncFuncCollectionAssertion(func!, expression); + } + /// /// Creates an assertion for an asynchronous function. /// Example: await Assert.That(async () => await GetValueAsync()).IsEqualTo(expected); diff --git a/TUnit.Assertions/Sources/AsyncFuncCollectionAssertion.cs b/TUnit.Assertions/Sources/AsyncFuncCollectionAssertion.cs new file mode 100644 index 0000000000..1e0512e4e7 --- /dev/null +++ b/TUnit.Assertions/Sources/AsyncFuncCollectionAssertion.cs @@ -0,0 +1,60 @@ +using System.Text; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Sources; + +/// +/// Source assertion for asynchronous functions that return collections. +/// This is the entry point for: Assert.That(async () => await GetCollectionAsync()) +/// Combines the lazy evaluation of AsyncFuncAssertion with the collection methods of CollectionAssertionBase. +/// Enables collection assertions like IsEmpty(), IsNotEmpty(), HasCount() on async lambda-wrapped collections. +/// +public class AsyncFuncCollectionAssertion : CollectionAssertionBase, TItem>, IDelegateAssertionSource> +{ + public AsyncFuncCollectionAssertion(Func?>> func, string? expression) + : base(CreateContext(func, expression)) + { + } + + private static AssertionContext> CreateContext(Func?>> func, string? expression) + { + var expressionBuilder = new StringBuilder(); + expressionBuilder.Append($"Assert.That({expression ?? "?"})"); + var evaluationContext = new EvaluationContext>(async () => + { + try + { + var result = await func().ConfigureAwait(false); + return (result, null); + } + catch (Exception ex) + { + return (default, ex); + } + }); + return new AssertionContext>(evaluationContext, expressionBuilder); + } + + /// + /// Asserts that the function throws the specified exception type (or subclass). + /// Example: await Assert.That(async () => await GetItemsAsync()).Throws<InvalidOperationException>(); + /// + public ThrowsAssertion Throws() where TException : Exception + { + Context.ExpressionBuilder.Append($".Throws<{typeof(TException).Name}>()"); + var mappedContext = Context.MapException(); + return new ThrowsAssertion(mappedContext!); + } + + /// + /// Asserts that the function throws exactly the specified exception type (not subclasses). + /// Example: await Assert.That(async () => await GetItemsAsync()).ThrowsExactly<InvalidOperationException>(); + /// + public ThrowsExactlyAssertion ThrowsExactly() where TException : Exception + { + Context.ExpressionBuilder.Append($".ThrowsExactly<{typeof(TException).Name}>()"); + var mappedContext = Context.MapException(); + return new ThrowsExactlyAssertion(mappedContext!); + } +} diff --git a/TUnit.Assertions/Sources/FuncCollectionAssertion.cs b/TUnit.Assertions/Sources/FuncCollectionAssertion.cs new file mode 100644 index 0000000000..dc4117846a --- /dev/null +++ b/TUnit.Assertions/Sources/FuncCollectionAssertion.cs @@ -0,0 +1,60 @@ +using System.Text; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Sources; + +/// +/// Source assertion for synchronous functions that return collections. +/// This is the entry point for: Assert.That(() => GetCollection()) +/// Combines the lazy evaluation of FuncAssertion with the collection methods of CollectionAssertionBase. +/// Enables collection assertions like IsEmpty(), IsNotEmpty(), HasCount() on lambda-wrapped collections. +/// +public class FuncCollectionAssertion : CollectionAssertionBase, TItem>, IDelegateAssertionSource> +{ + public FuncCollectionAssertion(Func?> func, string? expression) + : base(CreateContext(func, expression)) + { + } + + private static AssertionContext> CreateContext(Func?> func, string? expression) + { + var expressionBuilder = new StringBuilder(); + expressionBuilder.Append($"Assert.That({expression ?? "?"})"); + var evaluationContext = new EvaluationContext>(() => + { + try + { + var result = func(); + return Task.FromResult<(IEnumerable?, Exception?)>((result, null)); + } + catch (Exception ex) + { + return Task.FromResult<(IEnumerable?, Exception?)>((default, ex)); + } + }); + return new AssertionContext>(evaluationContext, expressionBuilder); + } + + /// + /// Asserts that the function throws the specified exception type (or subclass). + /// Example: await Assert.That(() => GetItems()).Throws<InvalidOperationException>(); + /// + public ThrowsAssertion Throws() where TException : Exception + { + Context.ExpressionBuilder.Append($".Throws<{typeof(TException).Name}>()"); + var mappedContext = Context.MapException(); + return new ThrowsAssertion(mappedContext!); + } + + /// + /// Asserts that the function throws exactly the specified exception type (not subclasses). + /// Example: await Assert.That(() => GetItems()).ThrowsExactly<InvalidOperationException>(); + /// + public ThrowsExactlyAssertion ThrowsExactly() where TException : Exception + { + Context.ExpressionBuilder.Append($".ThrowsExactly<{typeof(TException).Name}>()"); + var mappedContext = Context.MapException(); + return new ThrowsExactlyAssertion(mappedContext!); + } +} diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 9714184f79..c0667a7017 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -21,6 +21,10 @@ namespace public static . That(string? value, [.("value")] string? expression = null) { } [.(1)] public static . That(.? value, [.("value")] string? expression = null) { } + [.(1)] + public static . That(<.?> func, [.("func")] string? expression = null) { } + [.(1)] + public static . That(<.<.?>> func, [.("func")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } @@ -4333,6 +4337,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class AsyncFuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public AsyncFuncCollectionAssertion(<.<.?>> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public abstract class CollectionAssertionBase : ., ., . where TCollection : . { @@ -4412,6 +4424,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class FuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public FuncCollectionAssertion(<.?> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public class TaskAssertion : ., .<.>, ., . { public TaskAssertion(. task, string? expression) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 665faeb994..5b048ba2fb 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -19,6 +19,8 @@ namespace public static . That(. task, [.("task")] string? expression = null) { } public static . That(string? value, [.("value")] string? expression = null) { } public static . That(.? value, [.("value")] string? expression = null) { } + public static . That(<.?> func, [.("func")] string? expression = null) { } + public static . That(<.<.?>> func, [.("func")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } @@ -4313,6 +4315,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class AsyncFuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public AsyncFuncCollectionAssertion(<.<.?>> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public abstract class CollectionAssertionBase : ., ., . where TCollection : . { @@ -4392,6 +4402,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class FuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public FuncCollectionAssertion(<.?> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public class TaskAssertion : ., .<.>, ., . { public TaskAssertion(. task, string? expression) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index c8665a0b6a..3850ce1fb7 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -21,6 +21,10 @@ namespace public static . That(string? value, [.("value")] string? expression = null) { } [.(1)] public static . That(.? value, [.("value")] string? expression = null) { } + [.(1)] + public static . That(<.?> func, [.("func")] string? expression = null) { } + [.(1)] + public static . That(<.<.?>> func, [.("func")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } @@ -4333,6 +4337,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class AsyncFuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public AsyncFuncCollectionAssertion(<.<.?>> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public abstract class CollectionAssertionBase : ., ., . where TCollection : . { @@ -4412,6 +4424,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class FuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public FuncCollectionAssertion(<.?> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public class TaskAssertion : ., .<.>, ., . { public TaskAssertion(. task, string? expression) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index bd832a6fca..59c7d0f553 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -19,6 +19,8 @@ namespace public static . That(. task, [.("task")] string? expression = null) { } public static . That(string? value, [.("value")] string? expression = null) { } public static . That(.? value, [.("value")] string? expression = null) { } + public static . That(<.?> func, [.("func")] string? expression = null) { } + public static . That(<.<.?>> func, [.("func")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } @@ -3802,6 +3804,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class AsyncFuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public AsyncFuncCollectionAssertion(<.<.?>> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public abstract class CollectionAssertionBase : ., ., . where TCollection : . { @@ -3881,6 +3891,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class FuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public FuncCollectionAssertion(<.?> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public class TaskAssertion : ., .<.>, ., . { public TaskAssertion(. task, string? expression) { }