Skip to content

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

Description

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions