Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 21 additions & 0 deletions TUnit.Assertions.Tests/CollectionAssertionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,27 @@ 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 Count_WithInnerAssertion_Lambda_Collection()
{
Expand Down
26 changes: 26 additions & 0 deletions TUnit.Assertions/Conditions/AsyncEnumerableAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,32 @@ private static async Task<List<TItem>> MaterializeAsync(IAsyncEnumerable<TItem>
protected abstract AssertionResult CheckMaterialized(List<TItem> items);
}

internal class AsyncEnumerableNullAssertion<TItem> : AsyncEnumerableAssertionBase<TItem>
{
public AsyncEnumerableNullAssertion(AssertionContext<IAsyncEnumerable<TItem>> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<IAsyncEnumerable<TItem>> metadata)
=> NullCheck.CheckIsNull(metadata);

protected override string GetExpectation() => "to be null";
}

internal class AsyncEnumerableNotNullAssertion<TItem> : AsyncEnumerableAssertionBase<TItem>
{
public AsyncEnumerableNotNullAssertion(AssertionContext<IAsyncEnumerable<TItem>> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<IAsyncEnumerable<TItem>> metadata)
=> NullCheck.CheckIsNotNull(metadata);

protected override string GetExpectation() => "to not be null";
}

/// <summary>
/// Asserts that the async enumerable is empty or not empty.
/// </summary>
Expand Down
209 changes: 205 additions & 4 deletions TUnit.Assertions/Conditions/CollectionNullAssertion.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
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 null, preserving collection type information.
/// Extends CollectionAssertionBase to ensure .And and .Or return collection-specific continuations.
/// </summary>
internal class CollectionNullAssertion<TCollection, TItem> : CollectionAssertionBase<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
public CollectionNullAssertion(AssertionContext<TCollection> context)
: base(context)
{
}

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

protected override string GetExpectation() => "to be null";
}

/// <summary>
/// Asserts that a collection is not null, preserving collection type information.
/// Extends CollectionAssertionBase to ensure .And and .Or return collection-specific continuations.
Expand All @@ -17,16 +36,198 @@ public CollectionNotNullAssertion(AssertionContext<TCollection> context)
}

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

protected override string GetExpectation() => "to not be null";
}

internal class ListNullAssertion<TList, TItem> : ListAssertionBase<TList, TItem>
where TList : IList<TItem>
{
public ListNullAssertion(AssertionContext<TList> context)
: base(context)
{
var value = metadata.Value;
}

if (value != null)
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TList> metadata)
=> NullCheck.CheckIsNull(metadata);

protected override string GetExpectation() => "to be null";
}

internal class ListNotNullAssertion<TList, TItem> : ListAssertionBase<TList, TItem>
where TList : IList<TItem>
{
public ListNotNullAssertion(AssertionContext<TList> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TList> metadata)
=> NullCheck.CheckIsNotNull(metadata);

protected override string GetExpectation() => "to not be null";
}

internal class ReadOnlyListNullAssertion<TList, TItem> : ReadOnlyListAssertionBase<TList, TItem>
where TList : IReadOnlyList<TItem>
{
public ReadOnlyListNullAssertion(AssertionContext<TList> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TList> metadata)
=> NullCheck.CheckIsNull(metadata);

protected override string GetExpectation() => "to be null";
}

internal class ReadOnlyListNotNullAssertion<TList, TItem> : ReadOnlyListAssertionBase<TList, TItem>
where TList : IReadOnlyList<TItem>
{
public ReadOnlyListNotNullAssertion(AssertionContext<TList> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TList> metadata)
=> NullCheck.CheckIsNotNull(metadata);

protected override string GetExpectation() => "to not be null";
}

internal class DictionaryNullAssertion<TDictionary, TKey, TValue> : DictionaryAssertionBase<TDictionary, TKey, TValue>
where TDictionary : IReadOnlyDictionary<TKey, TValue>
where TKey : notnull
{
public DictionaryNullAssertion(AssertionContext<TDictionary> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TDictionary> metadata)
=> NullCheck.CheckIsNull(metadata);

protected override string GetExpectation() => "to be null";
}

internal class DictionaryNotNullAssertion<TDictionary, TKey, TValue> : DictionaryAssertionBase<TDictionary, TKey, TValue>
where TDictionary : IReadOnlyDictionary<TKey, TValue>
where TKey : notnull
{
public DictionaryNotNullAssertion(AssertionContext<TDictionary> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TDictionary> metadata)
=> NullCheck.CheckIsNotNull(metadata);

protected override string GetExpectation() => "to not be null";
}

internal class MutableDictionaryNullAssertion<TDictionary, TKey, TValue> : MutableDictionaryAssertionBase<TDictionary, TKey, TValue>
where TDictionary : IDictionary<TKey, TValue>
where TKey : notnull
{
public MutableDictionaryNullAssertion(AssertionContext<TDictionary> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TDictionary> metadata)
=> NullCheck.CheckIsNull(metadata);

protected override string GetExpectation() => "to be null";
}

internal class MutableDictionaryNotNullAssertion<TDictionary, TKey, TValue> : MutableDictionaryAssertionBase<TDictionary, TKey, TValue>
where TDictionary : IDictionary<TKey, TValue>
where TKey : notnull
{
public MutableDictionaryNotNullAssertion(AssertionContext<TDictionary> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TDictionary> metadata)
=> NullCheck.CheckIsNotNull(metadata);

protected override string GetExpectation() => "to not be null";
}

internal class SetNullAssertion<TSet, TItem> : SetAssertionBase<TSet, TItem>
where TSet : IEnumerable<TItem>
{
private readonly Func<TSet, ISetAdapter<TItem>> _adapterFactory;

public SetNullAssertion(
AssertionContext<TSet> context,
Func<TSet, ISetAdapter<TItem>> adapterFactory)
: base(context)
{
_adapterFactory = adapterFactory;
}

protected override ISetAdapter<TItem> CreateSetAdapter(TSet value) => _adapterFactory(value);

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

protected override string GetExpectation() => "to be null";
}

internal class SetNotNullAssertion<TSet, TItem> : SetAssertionBase<TSet, TItem>
where TSet : IEnumerable<TItem>
{
private readonly Func<TSet, ISetAdapter<TItem>> _adapterFactory;

public SetNotNullAssertion(
AssertionContext<TSet> context,
Func<TSet, ISetAdapter<TItem>> adapterFactory)
: base(context)
{
_adapterFactory = adapterFactory;
}

protected override ISetAdapter<TItem> CreateSetAdapter(TSet value) => _adapterFactory(value);

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

protected override string GetExpectation() => "to not be null";
}

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

if (metadata.Value is null)
{
return AssertionResult._passedTask;
}

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

protected override string GetExpectation() => "to not be null";
public static Task<AssertionResult> CheckIsNotNull<TValue>(EvaluationMetadata<TValue> metadata)
{
if (metadata.Exception != null)
{
return Task.FromResult(AssertionResult.Failed($"threw {metadata.Exception.GetType().Name}", metadata.Exception));
}

if (metadata.Value is not null)
{
return AssertionResult._passedTask;
}

return Task.FromResult(AssertionResult.Failed("value is null"));
}
}
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);
}

/// <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 AsyncEnumerableNotNullAssertion<TItem>(Context);
}

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

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

/// <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);
}

/// <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 CollectionNotNullAssertion<TCollection, TItem>(Context);
}

/// <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);
}

/// <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 DictionaryNotNullAssertion<TDictionary, TKey, TValue>(Context);
}

/// <summary>
/// Asserts that the dictionary contains the specified key.
/// This instance method enables calling ContainsKey with proper type inference.
Expand Down
Loading
Loading