Summary
Investigate whether MSTest should ship an equivalent of xUnit's Assert.Collection(collection, params Action<T>[] elementInspectors) API, which verifies that:
- A collection has exactly N elements (matching the number of inspectors), and
- Each element passes its corresponding inspector delegate (typically asserting on shape/structure of that element).
This came up while migrating dotnet/sdk test projects from xUnit to MSTest. In dotnet/sdk#54725 the BrowserScriptMiddlewareTest.InvokeAsync_ConfiguresHeaders test had to expand a single Assert.Collection call into 8 separate assertions because MSTest has no direct equivalent.
Before (xUnit)
Assert.Collection(
response.Headers.OrderBy(h => h.Key),
kvp =>
{
Assert.Equal("Cache-Control", kvp.Key);
Assert.Equal("no-store", kvp.Value);
},
kvp =>
{
Assert.Equal("Content-Length", kvp.Key);
Assert.NotEqual(0, kvp.Value.Count);
},
kvp =>
{
Assert.Equal("Content-Type", kvp.Key);
Assert.Equal("application/javascript; charset=utf-8", kvp.Value);
});
After (MSTest, current state — manual expansion)
var headers = response.Headers.OrderBy(h => h.Key).ToArray();
Assert.HasCount(3, headers);
Assert.AreEqual("Cache-Control", headers[0].Key);
Assert.AreEqual("no-store", headers[0].Value.ToString());
Assert.AreEqual("Content-Length", headers[1].Key);
Assert.AreNotEqual(0, headers[1].Value.Count);
Assert.AreEqual("Content-Type", headers[2].Key);
Assert.AreEqual("application/javascript; charset=utf-8", headers[2].Value.ToString());
The manual expansion loses two things the xUnit API provided for free:
- Index reporting on failure —
Assert.Collection reports which element index failed and shows the inspector source position; the indexed-access form just reports a plain AreEqual mismatch on headers[1].
- Element-count check that fails before NRE — if the collection has fewer elements than expected,
Assert.Collection produces a clear "expected N, got M" message; the manual form throws IndexOutOfRangeException/ArgumentOutOfRangeException instead, which is a worse diagnostic.
Proposed shape
public static partial class Assert
{
public static void Collection<T>(IEnumerable<T> collection, params Action<T>[] elementInspectors);
public static Task CollectionAsync<T>(IEnumerable<T> collection, params Func<T, Task>[] elementInspectors);
}
Failure message ideas:
- Count mismatch:
Assert.Collection failed. Expected collection to contain 3 element(s), but found 2.
- Inspector failure:
Assert.Collection failed at index 1. <wrapped inner assertion message>
Questions for the team
- Should we ship this? It's a frequent xUnit→MSTest migration pain point and there is no concise equivalent today.
- Naming.
Assert.Collection collides with the existing CollectionAssert class name; do we keep Collection (matches xUnit, lowest migration friction) or pick a different name (e.g., HasItems, Inspect, Elements)?
- Lazy enumeration. xUnit enumerates the source once into a list before counting/indexing. Should we do the same (safer for
IEnumerable<T> with side effects) or accept IReadOnlyList<T>/ICollection<T> overloads to avoid the allocation?
- Analyzer story. Should an MSTest analyzer suggest
Assert.Collection when it sees an Assert.HasCount(N, …) followed by N indexed assertions on the same collection?
- Relationship with
Assert.AreSequenceEqual. AreSequenceEqual covers exact equality; Collection covers per-element shape checks. Both are useful and not redundant — worth documenting the distinction.
Workaround today
Expand into Assert.HasCount + indexed asserts (as in dotnet/sdk#54725), accepting the diagnostic regression noted above.
References
Summary
Investigate whether MSTest should ship an equivalent of xUnit's
Assert.Collection(collection, params Action<T>[] elementInspectors)API, which verifies that:This came up while migrating
dotnet/sdktest projects from xUnit to MSTest. In dotnet/sdk#54725 theBrowserScriptMiddlewareTest.InvokeAsync_ConfiguresHeaderstest had to expand a singleAssert.Collectioncall into 8 separate assertions because MSTest has no direct equivalent.Before (xUnit)
After (MSTest, current state — manual expansion)
The manual expansion loses two things the xUnit API provided for free:
Assert.Collectionreports which element index failed and shows the inspector source position; the indexed-access form just reports a plainAreEqualmismatch onheaders[1].Assert.Collectionproduces a clear "expected N, got M" message; the manual form throwsIndexOutOfRangeException/ArgumentOutOfRangeExceptioninstead, which is a worse diagnostic.Proposed shape
Failure message ideas:
Assert.Collection failed. Expected collection to contain 3 element(s), but found 2.Assert.Collection failed at index 1. <wrapped inner assertion message>Questions for the team
Assert.Collectioncollides with the existingCollectionAssertclass name; do we keepCollection(matches xUnit, lowest migration friction) or pick a different name (e.g.,HasItems,Inspect,Elements)?IEnumerable<T>with side effects) or acceptIReadOnlyList<T>/ICollection<T>overloads to avoid the allocation?Assert.Collectionwhen it sees anAssert.HasCount(N, …)followed by N indexed assertions on the same collection?Assert.AreSequenceEqual.AreSequenceEqualcovers exact equality;Collectioncovers per-element shape checks. Both are useful and not redundant — worth documenting the distinction.Workaround today
Expand into
Assert.HasCount+ indexed asserts (as indotnet/sdk#54725), accepting the diagnostic regression noted above.References
BrowserScriptMiddlewareTest.cs(lines L55-R63)Assert.Collection)