Skip to content

Analyzer + code fixer: migrate CollectionAssert.* to Assert.* #8764

@Evangelink

Description

@Evangelink

Summary

Add a new analyzer + Roslyn code fixer that migrates calls on CollectionAssert.* to their Assert.* equivalents. Goal: give users a one-click path off the soft-obsolete CollectionAssert surface before we add [Obsolete] to it (tracked separately).

Motivation

CollectionAssert predates generics and has been effectively superseded by the newer Assert.AreSequenceEqual / Assert.AreEquivalent (and the existing Assert.AreAllDistinct, Assert.AreAllNotNull, Assert.AreAllOfType, Assert.Contains, Assert.DoesNotContain). Today there is no IDE friction that nudges users off CollectionAssert:

  • The class is not marked [Obsolete].
  • The existing MSTEST0065 only fires on Assert.AreEqual over collections, not on CollectionAssert.* itself.
  • Two methods (Assert.AreEquivalent and CollectionAssert.AreEquivalent) share a name but have opposite semantics (deep / order-sensitive vs. flat / order-insensitive), which is a real migration trap.

Shipping the analyzer + fixer first means that when (later) we mark CollectionAssert.* with [Obsolete], customers have an automated migration available and the warning rollout is painless.

Proposed mapping

From To
CollectionAssert.AreEqual(a, b) Assert.AreSequenceEqual(a, b)
CollectionAssert.AreNotEqual(a, b) Assert.AreNotSequenceEqual(a, b)
CollectionAssert.AreEquivalent(a, b) Assert.AreSequenceEqual(a, b, SequenceOrder.InAnyOrder)
CollectionAssert.AreNotEquivalent(a, b) Assert.AreNotSequenceEqual(a, b, SequenceOrder.InAnyOrder)
CollectionAssert.AllItemsAreNotNull(a) Assert.AreAllNotNull(a)
CollectionAssert.AllItemsAreUnique(a) Assert.AreAllDistinct(a)
CollectionAssert.AllItemsAreInstancesOfType(a, t) Assert.AreAllOfType(a, t)
CollectionAssert.Contains(a, x) Assert.Contains(x, a) (note argument order swap)
CollectionAssert.DoesNotContain(a, x) Assert.DoesNotContain(x, a) (note argument order swap)

Overloads that need careful handling

  • CollectionAssert.AreEqual(ICollection, ICollection, IComparer) and AreNotEqual(..., IComparer)Assert.AreSequenceEqual takes IEqualityComparer<T>, not the ordering IComparer. The fixer should either:
    • skip these calls with a clear diagnostic message ("manual migration required: replace IComparer with IEqualityComparer<T>"), or
    • offer an adapter (Comparer<T>.Create(...)-style) only when the comparer is a new XComparer() literal.
  • CollectionAssert.IsSubsetOf / IsNotSubsetOf — no direct Assert.* replacement today. Out of scope for this issue. Either leave un-migrated, or open a follow-up to add Assert.IsSubsetOf first.
  • Overloads with the legacy string format, params object[] args shape — collapse to interpolated $"..." message during migration.

Acceptance criteria

  • New analyzer registered with a new MSTEST diagnostic ID (next free: MSTEST0068 at time of writing — confirm against DiagnosticIds.cs before claiming the ID).
  • Category: Usage. Default severity: Info (we want low noise initially; can be escalated later in line with the broader plan).
  • Fires on every CollectionAssert.* method call covered by the mapping table above, except the explicitly-skipped overloads.
  • Code fixer ships in MSTest.Analyzers.CodeFixes and produces compilable, equivalent rewrites for every mapped overload.
  • Resource strings added to Resources.resx (Title / Message / Description); regenerate .xlf with dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf and the CodeFixes equivalent.
  • Unit tests in test/UnitTests/MSTest.Analyzers.UnitTests/ covering: positive case for each mapping row, negative case for the skipped IComparer overload, and a fixer round-trip test per row.
  • AnalyzerReleases.Unshipped.md entry.
  • Documentation entry under docs/ linking to the public analyzer docs page on learn.microsoft.com (page can land separately).

Out of scope

  • Adding [Obsolete] to the CollectionAssert APIs — tracked in a follow-up issue once this analyzer + fixer has shipped for at least one release.
  • Escalating MSTEST0065 to error — separate discussion.
  • Adding a new Assert.IsSubsetOf / Assert.IsNotSubsetOf to close the remaining gap — separate issue.

Context

This issue is the first of three Tier-1 follow-ups identified during a UX review of the equality / collection assertion surface. The companion issues are:

  1. (this one) — migration analyzer & fixer for CollectionAssert.*Assert.*.
  2. Tighten MSTEST0065 to fire on the argument type, not just the generic T.
  3. Add a code fixer for MSTEST0065.

The three should ideally land together (or at least in the same release) so users see "warning + fix" rather than "warning, figure it out yourself".

Metadata

Metadata

Assignees

Labels

area/analyzersMSTest.Analyzers Roslyn analyzers and code fixes.area/assertionAssert / StringAssert / CollectionAssert APIs.help wantedUp for grabs; can be claimed by commenting.
No fields configured for Feature.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions