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
299 changes: 299 additions & 0 deletions TUnit.Assertions.Tests/CollectionNullabilityWarningTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
namespace TUnit.Assertions.Tests;

/// <summary>
/// Tests to ensure that Assert.That() accepts nullable collection types without generating
/// nullability warnings (CS8604, CS8625, etc.). This validates the fix for GitHub issue reporting
/// false positive nullability warnings for List, IList, Dictionary, IDictionary types.
/// </summary>
public class CollectionNullabilityWarningTests
{
// ===================================
// List<T>? Tests
// ===================================

[Test]
public async Task NullableList_AcceptsNullableValue_NoWarning()
{
List<string>? list = ["a", "b", "c"];
await Assert.That(list).IsNotNull();
}

[Test]
public async Task NullableList_WithNullValue_IsNotNull_Fails()
{
List<string>? list = null;
var action = async () => await Assert.That(list).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableList_WithNullValue_IsNull_Passes()
{
List<string>? list = null;
await Assert.That(list).IsNull();
}

// ===================================
// IList<T>? Tests
// ===================================

[Test]
public async Task NullableIList_AcceptsNullableValue_NoWarning()
{
IList<string>? list = new List<string> { "a", "b", "c" };
await Assert.That(list).IsNotNull();
}

[Test]
public async Task NullableIList_WithNullValue_IsNotNull_Fails()
{
IList<string>? list = null;
var action = async () => await Assert.That(list).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableIList_WithNullValue_IsNull_Passes()
{
IList<string>? list = null;
await Assert.That(list).IsNull();
}

// ===================================
// Dictionary<TKey, TValue>? Tests
// ===================================

[Test]
public async Task NullableDictionary_AcceptsNullableValue_NoWarning()
{
Dictionary<string, int>? dict = new() { ["key"] = 1 };
await Assert.That(dict).IsNotNull();
}

[Test]
public async Task NullableDictionary_WithNullValue_IsNotNull_Fails()
{
Dictionary<string, int>? dict = null;
var action = async () => await Assert.That(dict).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableDictionary_WithNullValue_IsNull_Passes()
{
Dictionary<string, int>? dict = null;
await Assert.That(dict).IsNull();
}

// ===================================
// IDictionary<TKey, TValue>? Tests
// ===================================

[Test]
public async Task NullableIDictionary_AcceptsNullableValue_NoWarning()
{
IDictionary<string, int>? dict = new Dictionary<string, int> { ["key"] = 1 };
await Assert.That(dict).IsNotNull();
}

[Test]
public async Task NullableIDictionary_WithNullValue_IsNotNull_Fails()
{
IDictionary<string, int>? dict = null;
var action = async () => await Assert.That(dict).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableIDictionary_WithNullValue_IsNull_Passes()
{
IDictionary<string, int>? dict = null;
await Assert.That(dict).IsNull();
}

// ===================================
// IReadOnlyDictionary<TKey, TValue>? Tests
// ===================================

[Test]
public async Task NullableIReadOnlyDictionary_AcceptsNullableValue_NoWarning()
{
IReadOnlyDictionary<string, int>? dict = new Dictionary<string, int> { ["key"] = 1 };
await Assert.That(dict).IsNotNull();
}

[Test]
public async Task NullableIReadOnlyDictionary_WithNullValue_IsNotNull_Fails()
{
IReadOnlyDictionary<string, int>? dict = null;
var action = async () => await Assert.That(dict).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableIReadOnlyDictionary_WithNullValue_IsNull_Passes()
{
IReadOnlyDictionary<string, int>? dict = null;
await Assert.That(dict).IsNull();
}

// ===================================
// Array? Tests
// ===================================

[Test]
public async Task NullableArray_AcceptsNullableValue_NoWarning()
{
string[]? arr = ["a", "b", "c"];
await Assert.That(arr).IsNotNull();
}

[Test]
public async Task NullableArray_WithNullValue_IsNotNull_Fails()
{
string[]? arr = null;
var action = async () => await Assert.That(arr).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableArray_WithNullValue_IsNull_Passes()
{
string[]? arr = null;
await Assert.That(arr).IsNull();
}

// ===================================
// ISet<T>? Tests
// ===================================

[Test]
public async Task NullableISet_AcceptsNullableValue_NoWarning()
{
ISet<string>? set = new HashSet<string> { "a", "b", "c" };
await Assert.That(set).IsNotNull();
}

[Test]
public async Task NullableISet_WithNullValue_IsNotNull_Fails()
{
ISet<string>? set = null;
var action = async () => await Assert.That(set).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableISet_WithNullValue_IsNull_Passes()
{
ISet<string>? set = null;
await Assert.That(set).IsNull();
}

// ===================================
// HashSet<T>? Tests
// ===================================

[Test]
public async Task NullableHashSet_AcceptsNullableValue_NoWarning()
{
HashSet<string>? set = ["a", "b", "c"];
await Assert.That(set).IsNotNull();
}

[Test]
public async Task NullableHashSet_WithNullValue_IsNotNull_Fails()
{
HashSet<string>? set = null;
var action = async () => await Assert.That(set).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableHashSet_WithNullValue_IsNull_Passes()
{
HashSet<string>? set = null;
await Assert.That(set).IsNull();
}

#if NET5_0_OR_GREATER
// ===================================
// IReadOnlySet<T>? Tests
// ===================================

[Test]
public async Task NullableIReadOnlySet_AcceptsNullableValue_NoWarning()
{
IReadOnlySet<string>? set = new HashSet<string> { "a", "b", "c" };
await Assert.That(set).IsNotNull();
}

[Test]
public async Task NullableIReadOnlySet_WithNullValue_IsNotNull_Fails()
{
IReadOnlySet<string>? set = null;
var action = async () => await Assert.That(set).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableIReadOnlySet_WithNullValue_IsNull_Passes()
{
IReadOnlySet<string>? set = null;
await Assert.That(set).IsNull();
}
#endif

// ===================================
// IReadOnlyList<T>? Tests
// ===================================

[Test]
public async Task NullableIReadOnlyList_AcceptsNullableValue_NoWarning()
{
IReadOnlyList<string>? list = new List<string> { "a", "b", "c" };
await Assert.That(list).IsNotNull();
}

[Test]
public async Task NullableIReadOnlyList_WithNullValue_IsNotNull_Fails()
{
IReadOnlyList<string>? list = null;
var action = async () => await Assert.That(list).IsNotNull();
await Assert.That(action).Throws<AssertionException>();
}

[Test]
public async Task NullableIReadOnlyList_WithNullValue_IsNull_Passes()
{
IReadOnlyList<string>? list = null;
await Assert.That(list).IsNull();
}

// ===================================
// Method parameter tests (original issue scenario)
// ===================================

[Test]
public async Task MethodWithNullableListParameter_NoWarning()
{
List<string>? list = ["test"];
await VerifyNotNull(list);
}

[Test]
public async Task MethodWithNullableIDictionaryParameter_NoWarning()
{
IDictionary<string, int>? dict = new Dictionary<string, int> { ["key"] = 1 };
await VerifyDictionaryNotNull(dict);
}

private static async Task VerifyNotNull(List<string>? actual)
{
await Assert.That(actual).IsNotNull();
}

private static async Task VerifyDictionaryNotNull(IDictionary<string, int>? actual)
{
await Assert.That(actual).IsNotNull();
}
}
16 changes: 8 additions & 8 deletions TUnit.Assertions/Extensions/Assert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static class Assert
/// </summary>
[OverloadResolutionPriority(3)]
public static DictionaryAssertion<TKey, TValue> That<TKey, TValue>(
IReadOnlyDictionary<TKey, TValue> value,
IReadOnlyDictionary<TKey, TValue>? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
where TKey : notnull
{
Expand All @@ -35,7 +35,7 @@ public static DictionaryAssertion<TKey, TValue> That<TKey, TValue>(
/// </summary>
[OverloadResolutionPriority(2)]
public static MutableDictionaryAssertion<TKey, TValue> That<TKey, TValue>(
IDictionary<TKey, TValue> value,
IDictionary<TKey, TValue>? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
where TKey : notnull
{
Expand Down Expand Up @@ -108,7 +108,7 @@ public static ReadOnlyMemoryAssertion<TItem> That<TItem>(
/// </summary>
[OverloadResolutionPriority(2)]
public static ReadOnlySetAssertion<TItem> That<TItem>(
IReadOnlySet<TItem> value,
IReadOnlySet<TItem>? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
{
return new ReadOnlySetAssertion<TItem>(value, expression ?? "set");
Expand Down Expand Up @@ -138,7 +138,7 @@ public static AsyncEnumerableAssertion<TItem> That<TItem>(
/// </summary>
[OverloadResolutionPriority(2)]
public static SetAssertion<TItem> That<TItem>(
ISet<TItem> value,
ISet<TItem>? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
{
return new SetAssertion<TItem>(value, expression ?? "set");
Expand All @@ -153,7 +153,7 @@ public static SetAssertion<TItem> That<TItem>(
/// </summary>
[OverloadResolutionPriority(4)]
public static ListAssertion<TItem> That<TItem>(
IList<TItem> value,
IList<TItem>? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
{
return new ListAssertion<TItem>(value, expression ?? "list");
Expand All @@ -166,7 +166,7 @@ public static ListAssertion<TItem> That<TItem>(
/// </summary>
[OverloadResolutionPriority(5)]
public static CollectionAssertion<TItem> That<TItem>(
TItem[] value,
TItem[]? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
{
return new CollectionAssertion<TItem>(value!, expression);
Expand All @@ -181,7 +181,7 @@ public static CollectionAssertion<TItem> That<TItem>(
/// </summary>
[OverloadResolutionPriority(3)]
public static ReadOnlyListAssertion<TItem> That<TItem>(
IReadOnlyList<TItem> value,
IReadOnlyList<TItem>? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
{
return new ReadOnlyListAssertion<TItem>(value, expression ?? "readOnlyList");
Expand All @@ -195,7 +195,7 @@ public static ReadOnlyListAssertion<TItem> That<TItem>(
/// </summary>
[OverloadResolutionPriority(3)]
public static HashSetAssertion<TItem> That<TItem>(
HashSet<TItem> value,
HashSet<TItem>? value,
[CallerArgumentExpression(nameof(value))] string? expression = null)
{
return new HashSetAssertion<TItem>(value, expression ?? "hashSet");
Expand Down
6 changes: 3 additions & 3 deletions TUnit.Assertions/Sources/DictionaryAssertion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ namespace TUnit.Assertions.Sources;
public class DictionaryAssertion<TKey, TValue> : DictionaryAssertionBase<IReadOnlyDictionary<TKey, TValue>, TKey, TValue>
where TKey : notnull
{
public DictionaryAssertion(IReadOnlyDictionary<TKey, TValue> value, string? expression)
public DictionaryAssertion(IReadOnlyDictionary<TKey, TValue>? value, string? expression)
: base(CreateContext(value, expression))
{
}

private static AssertionContext<IReadOnlyDictionary<TKey, TValue>> CreateContext(
IReadOnlyDictionary<TKey, TValue> value,
IReadOnlyDictionary<TKey, TValue>? value,
string? expression)
{
var expressionBuilder = new StringBuilder();
expressionBuilder.Append($"Assert.That({expression ?? "?"})");
return new AssertionContext<IReadOnlyDictionary<TKey, TValue>>(value, expressionBuilder);
return new AssertionContext<IReadOnlyDictionary<TKey, TValue>>(value!, expressionBuilder);
}
}
Loading
Loading