Skip to content

Consider adding Assert.Collection(items, params Action<T>[] elementInspectors) #9079

@Evangelink

Description

@Evangelink

Summary

Investigate whether MSTest should ship an equivalent of xUnit's Assert.Collection(collection, params Action<T>[] elementInspectors) API, which verifies that:

  1. A collection has exactly N elements (matching the number of inspectors), and
  2. 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 failureAssert.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

  1. Should we ship this? It's a frequent xUnit→MSTest migration pain point and there is no concise equivalent today.
  2. 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)?
  3. 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?
  4. 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?
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions