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
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:
- (this one) — migration analyzer & fixer for
CollectionAssert.* → Assert.*.
- Tighten MSTEST0065 to fire on the argument type, not just the generic
T.
- 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".
Summary
Add a new analyzer + Roslyn code fixer that migrates calls on
CollectionAssert.*to theirAssert.*equivalents. Goal: give users a one-click path off the soft-obsoleteCollectionAssertsurface before we add[Obsolete]to it (tracked separately).Motivation
CollectionAssertpredates generics and has been effectively superseded by the newerAssert.AreSequenceEqual/Assert.AreEquivalent(and the existingAssert.AreAllDistinct,Assert.AreAllNotNull,Assert.AreAllOfType,Assert.Contains,Assert.DoesNotContain). Today there is no IDE friction that nudges users offCollectionAssert:[Obsolete].Assert.AreEqualover collections, not onCollectionAssert.*itself.Assert.AreEquivalentandCollectionAssert.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
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)andAreNotEqual(..., IComparer)—Assert.AreSequenceEqualtakesIEqualityComparer<T>, not the orderingIComparer. The fixer should either:IComparerwithIEqualityComparer<T>"), orComparer<T>.Create(...)-style) only when the comparer is anew XComparer()literal.CollectionAssert.IsSubsetOf/IsNotSubsetOf— no directAssert.*replacement today. Out of scope for this issue. Either leave un-migrated, or open a follow-up to addAssert.IsSubsetOffirst.string format, params object[] argsshape — collapse to interpolated$"..."message during migration.Acceptance criteria
DiagnosticIds.csbefore claiming the ID).Usage. Default severity:Info(we want low noise initially; can be escalated later in line with the broader plan).CollectionAssert.*method call covered by the mapping table above, except the explicitly-skipped overloads.MSTest.Analyzers.CodeFixesand produces compilable, equivalent rewrites for every mapped overload.Resources.resx(Title / Message / Description); regenerate.xlfwithdotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlfand the CodeFixes equivalent.test/UnitTests/MSTest.Analyzers.UnitTests/covering: positive case for each mapping row, negative case for the skippedICompareroverload, and a fixer round-trip test per row.AnalyzerReleases.Unshipped.mdentry.docs/linking to the public analyzer docs page on learn.microsoft.com (page can land separately).Out of scope
[Obsolete]to theCollectionAssertAPIs — tracked in a follow-up issue once this analyzer + fixer has shipped for at least one release.Assert.IsSubsetOf/Assert.IsNotSubsetOfto 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:
CollectionAssert.*→Assert.*.T.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".