Skip to content

Commit 112b41c

Browse files
Copilotthomhurst
andauthored
Fix false positive nullability warnings in Assert.That() for collection types (#4248)
* Fix nullability warnings in Assert.That() for collection types - Update Assert.That() overloads to accept nullable parameters: - IList<TItem>? - IDictionary<TKey, TValue>? - IReadOnlyDictionary<TKey, TValue>? - ISet<TItem>? - HashSet<TItem>? - IReadOnlySet<TItem>? (NET5+) - IReadOnlyList<TItem>? - TItem[]? - Update corresponding assertion class constructors to accept nullable values - Add CollectionNullabilityWarningTests to verify the fix - Update public API snapshots Fixes false positive nullability warning when calling Assert.That(list).IsNotNull() with nullable List<T>?, IList<T>?, Dictionary<TKey,TValue>?, IDictionary<TKey,TValue>?, etc. Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> * Add missing IReadOnlySet<T>? nullability tests Added 3 tests for IReadOnlySet<T>? to complete the test coverage for all nullable collection types supported by Assert.That(). Tests are conditionally compiled for .NET 5.0+ where IReadOnlySet<T> is available. Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
1 parent 2e8c2a1 commit 112b41c

9 files changed

Lines changed: 364 additions & 65 deletions
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
namespace TUnit.Assertions.Tests;
2+
3+
/// <summary>
4+
/// Tests to ensure that Assert.That() accepts nullable collection types without generating
5+
/// nullability warnings (CS8604, CS8625, etc.). This validates the fix for GitHub issue reporting
6+
/// false positive nullability warnings for List, IList, Dictionary, IDictionary types.
7+
/// </summary>
8+
public class CollectionNullabilityWarningTests
9+
{
10+
// ===================================
11+
// List<T>? Tests
12+
// ===================================
13+
14+
[Test]
15+
public async Task NullableList_AcceptsNullableValue_NoWarning()
16+
{
17+
List<string>? list = ["a", "b", "c"];
18+
await Assert.That(list).IsNotNull();
19+
}
20+
21+
[Test]
22+
public async Task NullableList_WithNullValue_IsNotNull_Fails()
23+
{
24+
List<string>? list = null;
25+
var action = async () => await Assert.That(list).IsNotNull();
26+
await Assert.That(action).Throws<AssertionException>();
27+
}
28+
29+
[Test]
30+
public async Task NullableList_WithNullValue_IsNull_Passes()
31+
{
32+
List<string>? list = null;
33+
await Assert.That(list).IsNull();
34+
}
35+
36+
// ===================================
37+
// IList<T>? Tests
38+
// ===================================
39+
40+
[Test]
41+
public async Task NullableIList_AcceptsNullableValue_NoWarning()
42+
{
43+
IList<string>? list = new List<string> { "a", "b", "c" };
44+
await Assert.That(list).IsNotNull();
45+
}
46+
47+
[Test]
48+
public async Task NullableIList_WithNullValue_IsNotNull_Fails()
49+
{
50+
IList<string>? list = null;
51+
var action = async () => await Assert.That(list).IsNotNull();
52+
await Assert.That(action).Throws<AssertionException>();
53+
}
54+
55+
[Test]
56+
public async Task NullableIList_WithNullValue_IsNull_Passes()
57+
{
58+
IList<string>? list = null;
59+
await Assert.That(list).IsNull();
60+
}
61+
62+
// ===================================
63+
// Dictionary<TKey, TValue>? Tests
64+
// ===================================
65+
66+
[Test]
67+
public async Task NullableDictionary_AcceptsNullableValue_NoWarning()
68+
{
69+
Dictionary<string, int>? dict = new() { ["key"] = 1 };
70+
await Assert.That(dict).IsNotNull();
71+
}
72+
73+
[Test]
74+
public async Task NullableDictionary_WithNullValue_IsNotNull_Fails()
75+
{
76+
Dictionary<string, int>? dict = null;
77+
var action = async () => await Assert.That(dict).IsNotNull();
78+
await Assert.That(action).Throws<AssertionException>();
79+
}
80+
81+
[Test]
82+
public async Task NullableDictionary_WithNullValue_IsNull_Passes()
83+
{
84+
Dictionary<string, int>? dict = null;
85+
await Assert.That(dict).IsNull();
86+
}
87+
88+
// ===================================
89+
// IDictionary<TKey, TValue>? Tests
90+
// ===================================
91+
92+
[Test]
93+
public async Task NullableIDictionary_AcceptsNullableValue_NoWarning()
94+
{
95+
IDictionary<string, int>? dict = new Dictionary<string, int> { ["key"] = 1 };
96+
await Assert.That(dict).IsNotNull();
97+
}
98+
99+
[Test]
100+
public async Task NullableIDictionary_WithNullValue_IsNotNull_Fails()
101+
{
102+
IDictionary<string, int>? dict = null;
103+
var action = async () => await Assert.That(dict).IsNotNull();
104+
await Assert.That(action).Throws<AssertionException>();
105+
}
106+
107+
[Test]
108+
public async Task NullableIDictionary_WithNullValue_IsNull_Passes()
109+
{
110+
IDictionary<string, int>? dict = null;
111+
await Assert.That(dict).IsNull();
112+
}
113+
114+
// ===================================
115+
// IReadOnlyDictionary<TKey, TValue>? Tests
116+
// ===================================
117+
118+
[Test]
119+
public async Task NullableIReadOnlyDictionary_AcceptsNullableValue_NoWarning()
120+
{
121+
IReadOnlyDictionary<string, int>? dict = new Dictionary<string, int> { ["key"] = 1 };
122+
await Assert.That(dict).IsNotNull();
123+
}
124+
125+
[Test]
126+
public async Task NullableIReadOnlyDictionary_WithNullValue_IsNotNull_Fails()
127+
{
128+
IReadOnlyDictionary<string, int>? dict = null;
129+
var action = async () => await Assert.That(dict).IsNotNull();
130+
await Assert.That(action).Throws<AssertionException>();
131+
}
132+
133+
[Test]
134+
public async Task NullableIReadOnlyDictionary_WithNullValue_IsNull_Passes()
135+
{
136+
IReadOnlyDictionary<string, int>? dict = null;
137+
await Assert.That(dict).IsNull();
138+
}
139+
140+
// ===================================
141+
// Array? Tests
142+
// ===================================
143+
144+
[Test]
145+
public async Task NullableArray_AcceptsNullableValue_NoWarning()
146+
{
147+
string[]? arr = ["a", "b", "c"];
148+
await Assert.That(arr).IsNotNull();
149+
}
150+
151+
[Test]
152+
public async Task NullableArray_WithNullValue_IsNotNull_Fails()
153+
{
154+
string[]? arr = null;
155+
var action = async () => await Assert.That(arr).IsNotNull();
156+
await Assert.That(action).Throws<AssertionException>();
157+
}
158+
159+
[Test]
160+
public async Task NullableArray_WithNullValue_IsNull_Passes()
161+
{
162+
string[]? arr = null;
163+
await Assert.That(arr).IsNull();
164+
}
165+
166+
// ===================================
167+
// ISet<T>? Tests
168+
// ===================================
169+
170+
[Test]
171+
public async Task NullableISet_AcceptsNullableValue_NoWarning()
172+
{
173+
ISet<string>? set = new HashSet<string> { "a", "b", "c" };
174+
await Assert.That(set).IsNotNull();
175+
}
176+
177+
[Test]
178+
public async Task NullableISet_WithNullValue_IsNotNull_Fails()
179+
{
180+
ISet<string>? set = null;
181+
var action = async () => await Assert.That(set).IsNotNull();
182+
await Assert.That(action).Throws<AssertionException>();
183+
}
184+
185+
[Test]
186+
public async Task NullableISet_WithNullValue_IsNull_Passes()
187+
{
188+
ISet<string>? set = null;
189+
await Assert.That(set).IsNull();
190+
}
191+
192+
// ===================================
193+
// HashSet<T>? Tests
194+
// ===================================
195+
196+
[Test]
197+
public async Task NullableHashSet_AcceptsNullableValue_NoWarning()
198+
{
199+
HashSet<string>? set = ["a", "b", "c"];
200+
await Assert.That(set).IsNotNull();
201+
}
202+
203+
[Test]
204+
public async Task NullableHashSet_WithNullValue_IsNotNull_Fails()
205+
{
206+
HashSet<string>? set = null;
207+
var action = async () => await Assert.That(set).IsNotNull();
208+
await Assert.That(action).Throws<AssertionException>();
209+
}
210+
211+
[Test]
212+
public async Task NullableHashSet_WithNullValue_IsNull_Passes()
213+
{
214+
HashSet<string>? set = null;
215+
await Assert.That(set).IsNull();
216+
}
217+
218+
#if NET5_0_OR_GREATER
219+
// ===================================
220+
// IReadOnlySet<T>? Tests
221+
// ===================================
222+
223+
[Test]
224+
public async Task NullableIReadOnlySet_AcceptsNullableValue_NoWarning()
225+
{
226+
IReadOnlySet<string>? set = new HashSet<string> { "a", "b", "c" };
227+
await Assert.That(set).IsNotNull();
228+
}
229+
230+
[Test]
231+
public async Task NullableIReadOnlySet_WithNullValue_IsNotNull_Fails()
232+
{
233+
IReadOnlySet<string>? set = null;
234+
var action = async () => await Assert.That(set).IsNotNull();
235+
await Assert.That(action).Throws<AssertionException>();
236+
}
237+
238+
[Test]
239+
public async Task NullableIReadOnlySet_WithNullValue_IsNull_Passes()
240+
{
241+
IReadOnlySet<string>? set = null;
242+
await Assert.That(set).IsNull();
243+
}
244+
#endif
245+
246+
// ===================================
247+
// IReadOnlyList<T>? Tests
248+
// ===================================
249+
250+
[Test]
251+
public async Task NullableIReadOnlyList_AcceptsNullableValue_NoWarning()
252+
{
253+
IReadOnlyList<string>? list = new List<string> { "a", "b", "c" };
254+
await Assert.That(list).IsNotNull();
255+
}
256+
257+
[Test]
258+
public async Task NullableIReadOnlyList_WithNullValue_IsNotNull_Fails()
259+
{
260+
IReadOnlyList<string>? list = null;
261+
var action = async () => await Assert.That(list).IsNotNull();
262+
await Assert.That(action).Throws<AssertionException>();
263+
}
264+
265+
[Test]
266+
public async Task NullableIReadOnlyList_WithNullValue_IsNull_Passes()
267+
{
268+
IReadOnlyList<string>? list = null;
269+
await Assert.That(list).IsNull();
270+
}
271+
272+
// ===================================
273+
// Method parameter tests (original issue scenario)
274+
// ===================================
275+
276+
[Test]
277+
public async Task MethodWithNullableListParameter_NoWarning()
278+
{
279+
List<string>? list = ["test"];
280+
await VerifyNotNull(list);
281+
}
282+
283+
[Test]
284+
public async Task MethodWithNullableIDictionaryParameter_NoWarning()
285+
{
286+
IDictionary<string, int>? dict = new Dictionary<string, int> { ["key"] = 1 };
287+
await VerifyDictionaryNotNull(dict);
288+
}
289+
290+
private static async Task VerifyNotNull(List<string>? actual)
291+
{
292+
await Assert.That(actual).IsNotNull();
293+
}
294+
295+
private static async Task VerifyDictionaryNotNull(IDictionary<string, int>? actual)
296+
{
297+
await Assert.That(actual).IsNotNull();
298+
}
299+
}

TUnit.Assertions/Extensions/Assert.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static class Assert
2121
/// </summary>
2222
[OverloadResolutionPriority(3)]
2323
public static DictionaryAssertion<TKey, TValue> That<TKey, TValue>(
24-
IReadOnlyDictionary<TKey, TValue> value,
24+
IReadOnlyDictionary<TKey, TValue>? value,
2525
[CallerArgumentExpression(nameof(value))] string? expression = null)
2626
where TKey : notnull
2727
{
@@ -35,7 +35,7 @@ public static DictionaryAssertion<TKey, TValue> That<TKey, TValue>(
3535
/// </summary>
3636
[OverloadResolutionPriority(2)]
3737
public static MutableDictionaryAssertion<TKey, TValue> That<TKey, TValue>(
38-
IDictionary<TKey, TValue> value,
38+
IDictionary<TKey, TValue>? value,
3939
[CallerArgumentExpression(nameof(value))] string? expression = null)
4040
where TKey : notnull
4141
{
@@ -108,7 +108,7 @@ public static ReadOnlyMemoryAssertion<TItem> That<TItem>(
108108
/// </summary>
109109
[OverloadResolutionPriority(2)]
110110
public static ReadOnlySetAssertion<TItem> That<TItem>(
111-
IReadOnlySet<TItem> value,
111+
IReadOnlySet<TItem>? value,
112112
[CallerArgumentExpression(nameof(value))] string? expression = null)
113113
{
114114
return new ReadOnlySetAssertion<TItem>(value, expression ?? "set");
@@ -138,7 +138,7 @@ public static AsyncEnumerableAssertion<TItem> That<TItem>(
138138
/// </summary>
139139
[OverloadResolutionPriority(2)]
140140
public static SetAssertion<TItem> That<TItem>(
141-
ISet<TItem> value,
141+
ISet<TItem>? value,
142142
[CallerArgumentExpression(nameof(value))] string? expression = null)
143143
{
144144
return new SetAssertion<TItem>(value, expression ?? "set");
@@ -153,7 +153,7 @@ public static SetAssertion<TItem> That<TItem>(
153153
/// </summary>
154154
[OverloadResolutionPriority(4)]
155155
public static ListAssertion<TItem> That<TItem>(
156-
IList<TItem> value,
156+
IList<TItem>? value,
157157
[CallerArgumentExpression(nameof(value))] string? expression = null)
158158
{
159159
return new ListAssertion<TItem>(value, expression ?? "list");
@@ -166,7 +166,7 @@ public static ListAssertion<TItem> That<TItem>(
166166
/// </summary>
167167
[OverloadResolutionPriority(5)]
168168
public static CollectionAssertion<TItem> That<TItem>(
169-
TItem[] value,
169+
TItem[]? value,
170170
[CallerArgumentExpression(nameof(value))] string? expression = null)
171171
{
172172
return new CollectionAssertion<TItem>(value!, expression);
@@ -181,7 +181,7 @@ public static CollectionAssertion<TItem> That<TItem>(
181181
/// </summary>
182182
[OverloadResolutionPriority(3)]
183183
public static ReadOnlyListAssertion<TItem> That<TItem>(
184-
IReadOnlyList<TItem> value,
184+
IReadOnlyList<TItem>? value,
185185
[CallerArgumentExpression(nameof(value))] string? expression = null)
186186
{
187187
return new ReadOnlyListAssertion<TItem>(value, expression ?? "readOnlyList");
@@ -195,7 +195,7 @@ public static ReadOnlyListAssertion<TItem> That<TItem>(
195195
/// </summary>
196196
[OverloadResolutionPriority(3)]
197197
public static HashSetAssertion<TItem> That<TItem>(
198-
HashSet<TItem> value,
198+
HashSet<TItem>? value,
199199
[CallerArgumentExpression(nameof(value))] string? expression = null)
200200
{
201201
return new HashSetAssertion<TItem>(value, expression ?? "hashSet");

TUnit.Assertions/Sources/DictionaryAssertion.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,17 @@ namespace TUnit.Assertions.Sources;
1313
public class DictionaryAssertion<TKey, TValue> : DictionaryAssertionBase<IReadOnlyDictionary<TKey, TValue>, TKey, TValue>
1414
where TKey : notnull
1515
{
16-
public DictionaryAssertion(IReadOnlyDictionary<TKey, TValue> value, string? expression)
16+
public DictionaryAssertion(IReadOnlyDictionary<TKey, TValue>? value, string? expression)
1717
: base(CreateContext(value, expression))
1818
{
1919
}
2020

2121
private static AssertionContext<IReadOnlyDictionary<TKey, TValue>> CreateContext(
22-
IReadOnlyDictionary<TKey, TValue> value,
22+
IReadOnlyDictionary<TKey, TValue>? value,
2323
string? expression)
2424
{
2525
var expressionBuilder = new StringBuilder();
2626
expressionBuilder.Append($"Assert.That({expression ?? "?"})");
27-
return new AssertionContext<IReadOnlyDictionary<TKey, TValue>>(value, expressionBuilder);
27+
return new AssertionContext<IReadOnlyDictionary<TKey, TValue>>(value!, expressionBuilder);
2828
}
2929
}

0 commit comments

Comments
 (0)