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
15 changes: 15 additions & 0 deletions TUnit.Assertions.Tests/AsyncEnumerableAssertionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ await Assert.That(items)
.Or.Contains(99); // First passes, so overall passes
}

[Test]
public async Task Test_AsyncEnumerable_NullAssertions_Preserve_Chaining()
{
var items = AsyncRange(1, 5);

await Assert.That(items)
.IsNotNull()
.And.Contains(3);

IAsyncEnumerable<int>? nullItems = null;
await Assert.That(nullItems!)
.IsNull()
.Or.Contains(3);
}

// Null handling
[Test]
public async Task Test_AsyncEnumerable_Null_Fails()
Expand Down
43 changes: 43 additions & 0 deletions TUnit.Assertions.Tests/CollectionAssertionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,49 @@ public async Task Count_WithInnerAssertion_AllMatch()
await Assert.That(items).Count(item => item.IsGreaterThan(0)).IsEqualTo(5);
}

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

await Assert.That(items).IsNotNull().And.ItemAt(0).IsEqualTo(1);

List<int>? nullItems = null;
await Assert.That(nullItems).IsNull().Or.ItemAt(0).IsEqualTo(1);
}

[Test]
public async Task NullAssertions_Preserve_Dictionary_And_Set_Chaining()
{
IDictionary<string, int> dictionary = new Dictionary<string, int> { ["one"] = 1 };
ISet<int> set = new HashSet<int> { 1 };

await Assert.That(dictionary).IsNotNull().And.ContainsKey("one");
await Assert.That(set).IsNotNull().And.IsSubsetOf([1, 2]);
}

[Test]
public async Task NullAssertions_IsNotNull_Fails_When_Null_Through_Chain()
{
List<int>? nullItems = null;

await Assert.That(async () =>
await Assert.That(nullItems).IsNotNull().And.Contains(1)
).Throws<AssertionException>()
.WithMessageContaining("to not be null");
}

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

await Assert.That(async () =>
await Assert.That(items).IsNull().Or.Contains(99)
).Throws<AssertionException>()
.WithMessageContaining("to be null");
}

[Test]
public async Task Count_WithInnerAssertion_Lambda_Collection()
{
Expand Down
9 changes: 9 additions & 0 deletions TUnit.Assertions/Conditions/AsyncEnumerableAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ private static async Task<List<TItem>> MaterializeAsync(IAsyncEnumerable<TItem>
protected abstract AssertionResult CheckMaterialized(List<TItem> items);
}

internal class AsyncEnumerableNullAssertion<TItem>(AssertionContext<IAsyncEnumerable<TItem>> context, bool expectNull)
: AsyncEnumerableAssertionBase<TItem>(context)
{
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<IAsyncEnumerable<TItem>> metadata)
=> NullCheck.Check(metadata, expectNull);

protected override string GetExpectation() => NullCheck.Expectation(expectNull);
}

/// <summary>
/// Asserts that the async enumerable is empty or not empty.
/// </summary>
Expand Down
89 changes: 78 additions & 11 deletions TUnit.Assertions/Conditions/CollectionNullAssertion.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,99 @@
using System.Collections;
using TUnit.Assertions.Abstractions;
using TUnit.Assertions.Core;
using TUnit.Assertions.Sources;

namespace TUnit.Assertions.Conditions;

/// <summary>
/// Asserts that a collection is not null, preserving collection type information.
/// Asserts that a collection is null (or not null), preserving collection type information.
/// Extends CollectionAssertionBase to ensure .And and .Or return collection-specific continuations.
/// </summary>
public class CollectionNotNullAssertion<TCollection, TItem> : CollectionAssertionBase<TCollection, TItem>
internal class CollectionNullAssertion<TCollection, TItem>(AssertionContext<TCollection> context, bool expectNull)
: CollectionAssertionBase<TCollection, TItem>(context)
where TCollection : IEnumerable<TItem>
{
public CollectionNotNullAssertion(AssertionContext<TCollection> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TCollection> metadata)
=> NullCheck.Check(metadata, expectNull);

protected override string GetExpectation() => NullCheck.Expectation(expectNull);
}

internal class ListNullAssertion<TList, TItem>(AssertionContext<TList> context, bool expectNull)
: ListAssertionBase<TList, TItem>(context)
where TList : IList<TItem>
{
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TList> metadata)
=> NullCheck.Check(metadata, expectNull);

protected override string GetExpectation() => NullCheck.Expectation(expectNull);
}

internal class ReadOnlyListNullAssertion<TList, TItem>(AssertionContext<TList> context, bool expectNull)
: ReadOnlyListAssertionBase<TList, TItem>(context)
where TList : IReadOnlyList<TItem>
{
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TList> metadata)
=> NullCheck.Check(metadata, expectNull);

protected override string GetExpectation() => NullCheck.Expectation(expectNull);
}

internal class DictionaryNullAssertion<TDictionary, TKey, TValue>(AssertionContext<TDictionary> context, bool expectNull)
: DictionaryAssertionBase<TDictionary, TKey, TValue>(context)
where TDictionary : IReadOnlyDictionary<TKey, TValue>
where TKey : notnull
{
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TDictionary> metadata)
=> NullCheck.Check(metadata, expectNull);

protected override string GetExpectation() => NullCheck.Expectation(expectNull);
}

internal class MutableDictionaryNullAssertion<TDictionary, TKey, TValue>(AssertionContext<TDictionary> context, bool expectNull)
: MutableDictionaryAssertionBase<TDictionary, TKey, TValue>(context)
where TDictionary : IDictionary<TKey, TValue>
where TKey : notnull
{
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TDictionary> metadata)
=> NullCheck.Check(metadata, expectNull);

protected override string GetExpectation() => NullCheck.Expectation(expectNull);
}

internal class SetNullAssertion<TSet, TItem>(
AssertionContext<TSet> context,
Func<TSet, ISetAdapter<TItem>> adapterFactory,
bool expectNull)
: SetAssertionBase<TSet, TItem>(context)
where TSet : IEnumerable<TItem>
{
// adapterFactory is never invoked on the null-check path (the set is never materialized),
// but SetAssertionBase requires CreateSetAdapter, so it must be supplied.
protected override ISetAdapter<TItem> CreateSetAdapter(TSet value) => adapterFactory(value);

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TSet> metadata)
=> NullCheck.Check(metadata, expectNull);

protected override string GetExpectation() => NullCheck.Expectation(expectNull);
}

internal static class NullCheck
{
public static Task<AssertionResult> Check<TValue>(EvaluationMetadata<TValue> metadata, bool expectNull)
{
var value = metadata.Value;
if (metadata.Exception != null)
{
return Task.FromResult(AssertionResult.Failed($"threw {metadata.Exception.GetType().Name}", metadata.Exception));
}

if (value != null)
if (expectNull ? metadata.Value is null : metadata.Value is not null)
{
return AssertionResult._passedTask;
}

return Task.FromResult(AssertionResult.Failed("value is null"));
return Task.FromResult(AssertionResult.Failed(expectNull ? "value is not null" : "value is null"));
}

protected override string GetExpectation() => "to not be null";
public static string Expectation(bool expectNull) => expectNull ? "to be null" : "to not be null";
}
2 changes: 1 addition & 1 deletion TUnit.Assertions/Conditions/NullAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m
{
if (metadata.Exception != null)
{
return Task.FromResult(AssertionResult.Failed("received null"));
return Task.FromResult(AssertionResult.Failed($"threw {metadata.Exception.GetType().Name}", metadata.Exception));
}

var value = metadata.Value;
Expand Down
16 changes: 0 additions & 16 deletions TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,6 @@ public static NotNullAssertion<TValue> IsNotNull<TValue>(
return new NotNullAssertion<TValue>(mappedContext);
}

/// <summary>
/// Asserts that a collection is not null, preserving collection type information.
/// Returns a collection-aware assertion that maintains TItem type for proper chaining.
/// This overload enables: Assert.That(collection).IsNotNull().And.Contains(x => predicate).
/// </summary>
public static CollectionNotNullAssertion<TCollection, TItem> IsNotNull<TCollection, TItem>(
this CollectionAssertionBase<TCollection, TItem> source)
where TCollection : class, IEnumerable<TItem>
{
var assertionSource = (IAssertionSource<TCollection>)source;
assertionSource.Context.ExpressionBuilder.Append(".IsNotNull()");
// Map from TCollection? to TCollection (nullable to non-nullable)
var mappedContext = assertionSource.Context.Map((TCollection? v) => v!);
return new CollectionNotNullAssertion<TCollection, TItem>(mappedContext);
}

/// <summary>
/// Alias for IsEqualTo - asserts that the value is equal to the expected value.
/// Works with assertions, And, and Or continuations!
Expand Down
18 changes: 18 additions & 0 deletions TUnit.Assertions/Sources/AsyncEnumerableAssertionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ private protected AsyncEnumerableAssertionBase(

protected override string GetExpectation() => "async enumerable assertion";

/// <summary>
/// Asserts that the async enumerable is null while preserving async-enumerable-specific chaining.
/// </summary>
public AsyncEnumerableAssertionBase<TItem> IsNull()
{
Context.ExpressionBuilder.Append(".IsNull()");
return new AsyncEnumerableNullAssertion<TItem>(Context, expectNull: true);
}

/// <summary>
/// Asserts that the async enumerable is not null while preserving async-enumerable-specific chaining.
/// </summary>
public AsyncEnumerableAssertionBase<TItem> IsNotNull()
{
Context.ExpressionBuilder.Append(".IsNotNull()");
return new AsyncEnumerableNullAssertion<TItem>(Context, expectNull: false);
}

/// <summary>
/// Asserts that the async enumerable is empty.
/// Example: await Assert.That(asyncEnumerable).IsEmpty();
Expand Down
23 changes: 23 additions & 0 deletions TUnit.Assertions/Sources/CollectionAssertionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ private protected CollectionAssertionBase(

protected override string GetExpectation() => "collection assertion";

// Subclasses hide IsNull/IsNotNull with `public new` to narrow the return type to their own
// base (e.g. ListAssertionBase). This is the same idiom used by And/Or below; virtual+override
// with covariant returns is avoided because covariant returns are not honored at runtime on the
// netstandard2.0 target consumed by .NET Framework.

/// <summary>
/// Asserts that the collection is null while preserving collection-specific chaining.
/// </summary>
public CollectionAssertionBase<TCollection, TItem> IsNull()
{
Context.ExpressionBuilder.Append(".IsNull()");
return new CollectionNullAssertion<TCollection, TItem>(Context, expectNull: true);
}

/// <summary>
/// Asserts that the collection is not null while preserving collection-specific chaining.
/// </summary>
public CollectionAssertionBase<TCollection, TItem> IsNotNull()
{
Context.ExpressionBuilder.Append(".IsNotNull()");
return new CollectionNullAssertion<TCollection, TItem>(Context, expectNull: false);
}

/// <summary>
/// Asserts that the collection is of the specified type and returns an assertion on the casted value.
/// This allows chaining additional assertions on the typed value.
Expand Down
18 changes: 18 additions & 0 deletions TUnit.Assertions/Sources/DictionaryAssertionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ private protected DictionaryAssertionBase(

protected override string GetExpectation() => "dictionary assertion";

/// <summary>
/// Asserts that the dictionary is null while preserving dictionary-specific chaining.
/// </summary>
public new DictionaryAssertionBase<TDictionary, TKey, TValue> IsNull()
{
Context.ExpressionBuilder.Append(".IsNull()");
return new DictionaryNullAssertion<TDictionary, TKey, TValue>(Context, expectNull: true);
}

/// <summary>
/// Asserts that the dictionary is not null while preserving dictionary-specific chaining.
/// </summary>
public new DictionaryAssertionBase<TDictionary, TKey, TValue> IsNotNull()
{
Context.ExpressionBuilder.Append(".IsNotNull()");
return new DictionaryNullAssertion<TDictionary, TKey, TValue>(Context, expectNull: false);
}

/// <summary>
/// Asserts that the dictionary contains the specified key.
/// This instance method enables calling ContainsKey with proper type inference.
Expand Down
18 changes: 18 additions & 0 deletions TUnit.Assertions/Sources/ListAssertionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ private protected ListAssertionBase(
{
}

/// <summary>
/// Asserts that the list is null while preserving list-specific chaining.
/// </summary>
public new ListAssertionBase<TList, TItem> IsNull()
{
Context.ExpressionBuilder.Append(".IsNull()");
return new ListNullAssertion<TList, TItem>(Context, expectNull: true);
}

/// <summary>
/// Asserts that the list is not null while preserving list-specific chaining.
/// </summary>
public new ListAssertionBase<TList, TItem> IsNotNull()
{
Context.ExpressionBuilder.Append(".IsNotNull()");
return new ListNullAssertion<TList, TItem>(Context, expectNull: false);
}

/// <summary>
/// Asserts that the item at the specified index equals the expected value.
/// Example: await Assert.That(list).HasItemAt(0, "expected");
Expand Down
18 changes: 18 additions & 0 deletions TUnit.Assertions/Sources/MutableDictionaryAssertionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ private protected MutableDictionaryAssertionBase(

protected override string GetExpectation() => "dictionary assertion";

/// <summary>
/// Asserts that the dictionary is null while preserving mutable-dictionary-specific chaining.
/// </summary>
public new MutableDictionaryAssertionBase<TDictionary, TKey, TValue> IsNull()
{
Context.ExpressionBuilder.Append(".IsNull()");
return new MutableDictionaryNullAssertion<TDictionary, TKey, TValue>(Context, expectNull: true);
}

/// <summary>
/// Asserts that the dictionary is not null while preserving mutable-dictionary-specific chaining.
/// </summary>
public new MutableDictionaryAssertionBase<TDictionary, TKey, TValue> IsNotNull()
{
Context.ExpressionBuilder.Append(".IsNotNull()");
return new MutableDictionaryNullAssertion<TDictionary, TKey, TValue>(Context, expectNull: false);
}

/// <summary>
/// Asserts that the dictionary contains the specified key.
/// Example: await Assert.That(dictionary).ContainsKey("key1");
Expand Down
18 changes: 18 additions & 0 deletions TUnit.Assertions/Sources/ReadOnlyListAssertionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ private protected ReadOnlyListAssertionBase(
context.SetPendingLink(previousAssertion, combinerType);
}

/// <summary>
/// Asserts that the list is null while preserving read-only-list-specific chaining.
/// </summary>
public new ReadOnlyListAssertionBase<TList, TItem> IsNull()
{
Context.ExpressionBuilder.Append(".IsNull()");
return new ReadOnlyListNullAssertion<TList, TItem>(Context, expectNull: true);
}

/// <summary>
/// Asserts that the list is not null while preserving read-only-list-specific chaining.
/// </summary>
public new ReadOnlyListAssertionBase<TList, TItem> IsNotNull()
{
Context.ExpressionBuilder.Append(".IsNotNull()");
return new ReadOnlyListNullAssertion<TList, TItem>(Context, expectNull: false);
}

/// <summary>
/// Asserts that the list has an item at the specified index that equals the expected value.
/// Example: await Assert.That(list).HasItemAt(0, "first");
Expand Down
Loading
Loading