Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions TUnit.Assertions.Tests/CollectionAssertionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
namespace TUnit.Assertions.Tests;

public class CollectionAssertionTests
{
[Test]
public async Task IsEmpty()
{
var items = new List<int>();

await Assert.That(items).IsEmpty();
}

[Test]
public async Task IsEmpty2()
{
var items = new List<int>();

await Assert.That(() => items).IsEmpty();
}

[Test]
public async Task Count()
{
var items = new List<int>();

await Assert.That(items).Count().IsEqualTo(0);
}

[Test]
public async Task Count2()
{
var items = new List<int>();

await Assert.That(() => items).Count().IsEqualTo(0);
}
}
229 changes: 229 additions & 0 deletions TUnit.Assertions.Tests/FuncCollectionAssertionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
using TUnit.Assertions.Exceptions;

namespace TUnit.Assertions.Tests;

/// <summary>
/// Tests for FuncCollectionAssertion - verifies that collection assertions work when
/// collections are wrapped in lambdas. Addresses GitHub issue #3910.
/// </summary>
public class FuncCollectionAssertionTests
{
[Test]
public async Task Lambda_Collection_IsEmpty_Passes_For_Empty_Collection()
{
var items = new List<int>();
await Assert.That(() => items).IsEmpty();
}

[Test]
public async Task Lambda_Collection_IsEmpty_Fails_For_NonEmpty_Collection()
{
var items = new List<int> { 1, 2, 3 };
await Assert.That(async () => await Assert.That(() => items).IsEmpty())
.Throws<TUnitAssertionException>();
}

[Test]
public async Task Lambda_Collection_IsNotEmpty_Passes_For_NonEmpty_Collection()
{
var items = new List<int> { 1, 2, 3 };
await Assert.That(() => items).IsNotEmpty();
}

[Test]
public async Task Lambda_Collection_IsNotEmpty_Fails_For_Empty_Collection()
{
var items = new List<int>();
await Assert.That(async () => await Assert.That(() => items).IsNotEmpty())
.Throws<TUnitAssertionException>();
}

[Test]
public async Task Lambda_Collection_Count_IsEqualTo_Passes()
{
var items = new List<int> { 1, 2, 3 };
await Assert.That(() => items).Count().IsEqualTo(3);
}

[Test]
public async Task Lambda_Collection_Count_IsGreaterThan_Passes()
{
var items = new List<int> { 1, 2, 3, 4, 5 };
await Assert.That(() => items).Count().IsGreaterThan(3);
}

[Test]
public async Task Lambda_Collection_Contains_Passes()
{
var items = new List<int> { 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<int> { 1, 2, 3 };
await Assert.That(async () => await Assert.That(() => items).Contains(99))
.Throws<TUnitAssertionException>();
}

[Test]
public async Task Lambda_Collection_DoesNotContain_Passes()
{
var items = new List<int> { 1, 2, 3 };
await Assert.That(() => items).DoesNotContain(99);
}

[Test]
public async Task Lambda_Collection_HasSingleItem_Passes()
{
var items = new List<int> { 42 };
await Assert.That(() => items).HasSingleItem();
}

[Test]
public async Task Lambda_Collection_All_Passes()
{
var items = new List<int> { 2, 4, 6 };
await Assert.That(() => items).All(x => x % 2 == 0);
}

[Test]
public async Task Lambda_Collection_Any_Passes()
{
var items = new List<int> { 1, 2, 3 };
await Assert.That(() => items).Any(x => x > 2);
}

[Test]
public async Task Lambda_Collection_IsInOrder_Passes()
{
var items = new List<int> { 1, 2, 3, 4, 5 };
await Assert.That(() => items).IsInOrder();
}

[Test]
public async Task Lambda_Collection_IsInDescendingOrder_Passes()
{
var items = new List<int> { 5, 4, 3, 2, 1 };
await Assert.That(() => items).IsInDescendingOrder();
}

[Test]
public async Task Lambda_Collection_HasDistinctItems_Passes()
{
var items = new List<int> { 1, 2, 3 };
await Assert.That(() => items).HasDistinctItems();
}

[Test]
public async Task Lambda_Collection_Throws_Passes_When_Exception_Thrown()
{
await Assert.That(() => ThrowingMethod()).Throws<InvalidOperationException>();
}

[Test]
public async Task Lambda_Collection_Chaining_With_And()
{
var items = new List<int> { 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<string>();
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<int> items = Enumerable.Empty<int>();
await Assert.That(() => items).IsEmpty();
}

[Test]
public async Task Lambda_HashSet_Contains_Passes()
{
var items = new HashSet<int> { 1, 2, 3 };
await Assert.That(() => items).Contains(2);
}

private static IEnumerable<int> 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<InvalidOperationException>();
}

[Test]
public async Task AsyncLambda_Collection_Chaining_With_And()
{
await Assert.That(async () => await GetCollectionAsync())
.IsNotEmpty()
.And.Contains(2)
.And.HasDistinctItems();
}

private static Task<IEnumerable<int>> GetEmptyCollectionAsync()
{
return Task.FromResult<IEnumerable<int>>(new List<int>());
}

private static Task<IEnumerable<int>> GetCollectionAsync()
{
return Task.FromResult<IEnumerable<int>>(new List<int> { 1, 2, 3 });
}

private static async Task<IEnumerable<int>> ThrowingMethodAsync()
{
await Task.Yield();
throw new InvalidOperationException("Test exception");
}
}
28 changes: 28 additions & 0 deletions TUnit.Assertions/Extensions/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ public static ValueAssertion<TValue> That<TValue>(
return new ValueAssertion<TValue>(value, expression);
}

/// <summary>
/// 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);
/// </summary>
[OverloadResolutionPriority(1)]
public static FuncCollectionAssertion<TItem> That<TItem>(
Func<IEnumerable<TItem>?> func,
[CallerArgumentExpression(nameof(func))] string? expression = null)
{
return new FuncCollectionAssertion<TItem>(func!, expression);
}

/// <summary>
/// Creates an assertion for a synchronous function.
/// Example: await Assert.That(() => GetValue()).IsGreaterThan(10);
Expand All @@ -97,6 +111,20 @@ public static FuncAssertion<TValue> That<TValue>(
return new FuncAssertion<TValue>(func, expression);
}

/// <summary>
/// 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);
/// </summary>
[OverloadResolutionPriority(1)]
public static AsyncFuncCollectionAssertion<TItem> That<TItem>(
Func<Task<IEnumerable<TItem>?>> func,
[CallerArgumentExpression(nameof(func))] string? expression = null)
{
return new AsyncFuncCollectionAssertion<TItem>(func!, expression);
}

/// <summary>
/// Creates an assertion for an asynchronous function.
/// Example: await Assert.That(async () => await GetValueAsync()).IsEqualTo(expected);
Expand Down
60 changes: 60 additions & 0 deletions TUnit.Assertions/Sources/AsyncFuncCollectionAssertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.Text;
using TUnit.Assertions.Conditions;
using TUnit.Assertions.Core;

namespace TUnit.Assertions.Sources;

/// <summary>
/// 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.
/// </summary>
public class AsyncFuncCollectionAssertion<TItem> : CollectionAssertionBase<IEnumerable<TItem>, TItem>, IDelegateAssertionSource<IEnumerable<TItem>>
{
public AsyncFuncCollectionAssertion(Func<Task<IEnumerable<TItem>?>> func, string? expression)
: base(CreateContext(func, expression))
{
}

private static AssertionContext<IEnumerable<TItem>> CreateContext(Func<Task<IEnumerable<TItem>?>> func, string? expression)
{
var expressionBuilder = new StringBuilder();
expressionBuilder.Append($"Assert.That({expression ?? "?"})");
var evaluationContext = new EvaluationContext<IEnumerable<TItem>>(async () =>
{
try
{
var result = await func().ConfigureAwait(false);
return (result, null);
}
catch (Exception ex)
{
return (default, ex);
}
});
return new AssertionContext<IEnumerable<TItem>>(evaluationContext, expressionBuilder);
}

/// <summary>
/// Asserts that the function throws the specified exception type (or subclass).
/// Example: await Assert.That(async () => await GetItemsAsync()).Throws&lt;InvalidOperationException&gt;();
/// </summary>
public ThrowsAssertion<TException> Throws<TException>() where TException : Exception
{
Context.ExpressionBuilder.Append($".Throws<{typeof(TException).Name}>()");
var mappedContext = Context.MapException<TException>();
return new ThrowsAssertion<TException>(mappedContext!);
}

/// <summary>
/// Asserts that the function throws exactly the specified exception type (not subclasses).
/// Example: await Assert.That(async () => await GetItemsAsync()).ThrowsExactly&lt;InvalidOperationException&gt;();
/// </summary>
public ThrowsExactlyAssertion<TException> ThrowsExactly<TException>() where TException : Exception
{
Context.ExpressionBuilder.Append($".ThrowsExactly<{typeof(TException).Name}>()");
var mappedContext = Context.MapException<TException>();
return new ThrowsExactlyAssertion<TException>(mappedContext!);
}
}
Loading
Loading