Analyzers¶
Installing Axiom.Assertions gives you the Axiom Roslyn analyzers/code fixes automatically. That is the default install path for most users.
Axiom.Analyzers still exists as an optional standalone package if you only want the diagnostics without the runtime assertion library.
dotnet add package Axiom.Assertions
Install the standalone package only if you want the diagnostics on their own:
dotnet add package Axiom.Analyzers
If you are developing against a direct ProjectReference to Axiom.Assertions.csproj, add the analyzer projects explicitly as analyzer references in the consuming project:
<ItemGroup>
<ProjectReference Include=".../Axiom.Assertions.csproj" />
<ProjectReference Include=".../Axiom.Analyzers.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
SetTargetFramework="TargetFramework=netstandard2.0" />
<ProjectReference Include=".../Axiom.Analyzers.CodeFixes.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
SetTargetFramework="TargetFramework=netstandard2.0" />
</ItemGroup>
That keeps the normal NuGet package flow unchanged while giving local project-reference consumers the same analyzer/code-fix assets explicitly.
The current rules focus on a few high-value areas:
- ignored async Axiom assertion results
Batchinstances created withoutusing- high-confidence xUnit
Assert.*migration suggestions - conservative NUnit
Assert.That(...)migration suggestions - conservative MSTest
Assert.*migration suggestions
Migration Coverage At A Glance¶
The migration analyzers are intentionally conservative. They only suggest rewrites when the target Axiom assertion preserves the old value flow and assertion semantics.
| Framework | Current migration coverage |
|---|---|
| xUnit | scalar assertions, strings including safe StringComparison overloads, dictionary key lookup, Single(...), synchronous exceptions, and awaited async exception assertions |
| NUnit | common Is.*, Does.*, and Has.Count.EqualTo(...) constraints, plus ordered/range/type constraints and async exception assertions in async contexts |
| MSTest | scalar assertions, reference/type checks, StringAssert, simple CollectionAssert containment, ordered/range checks, and awaited async exception assertions |
For a shorter migration planning view, see Migrating to Axiom.
Async Assertions Must Be Awaited¶
Rule: AXM0001
This rule flags ignored async Axiom assertion calls from:
AsyncActionAssertionsAsyncFunctionAssertions<T>TaskAssertionsTaskAssertions<T>
Examples include:
ThrowAsync(...)NotThrowAsync()Succeed()SucceedWithin(...)BeFaultedWith(...)
Async Axiom assertions are lazy until their returned ValueTask is awaited or returned. If the result is ignored, the test can appear to pass even though the assertion never executed.
Before / After¶
Before:
loader.Should().Succeed();
After:
await loader.Should().Succeed();
The analyzer also offers a code fix in async contexts where prepending await is safe.
Batch Must Be Disposed¶
Rule: AXM0002
This rule flags Batch instances created without using.
Before:
var batch = Assert.Batch("user");
user.Name.Should().NotBeNull();
After:
using var batch = Assert.Batch("user");
user.Name.Should().NotBeNull();
Batch flushes aggregated failures when it is disposed. If it is created without using, failures may never be emitted at the end of the scope.
The analyzer offers a code fix for the common local declaration case by converting var batch = ...; to using var batch = ...;.
xUnit Assert Migration Suggestions¶
Rules:
AXM1001forAssert.Equal(expected, actual)and safe non-string comparer overloadsAXM1002forAssert.NotEqual(expected, actual)and safe non-string comparer overloadsAXM1003forAssert.Null(value)AXM1004forAssert.NotNull(value)AXM1005forAssert.True(condition)AXM1006forAssert.False(condition)AXM1007forAssert.Empty(subject)AXM1008forAssert.NotEmpty(subject)AXM1009forAssert.Contains(item, collection)AXM1010forAssert.DoesNotContain(item, collection)AXM1011forAssert.Single(subject), appending.SingleItemwhen the single item is usedAXM1012forAssert.Same(expected, actual)AXM1013forAssert.NotSame(expected, actual)AXM1014forAssert.Throws<TException>(...), including non-null constantparamName+Actionoverloads and appending.Thrownwhen the exception is usedAXM1015forAssert.IsType<T>(actual)AXM1016forAssert.IsAssignableFrom<T>(actual)AXM1017forAssert.Contains(expectedSubstring, actualString)andStringComparisonoverloadsAXM1018forAssert.DoesNotContain(expectedSubstring, actualString)andStringComparisonoverloadsAXM1019forAssert.Single(collection, predicate), appending.SingleItemwhen the matched item is usedAXM1020forAssert.Contains(key, dictionary), appending.WhoseValuewhen the associated value is usedAXM1021forAssert.DoesNotContain(key, dictionary)AXM1022forAssert.StartsWith(expectedPrefix, actualString)andStringComparisonoverloads when the prefix is an obvious non-null constant stringAXM1023forAssert.EndsWith(expectedSuffix, actualString)andStringComparisonoverloads when the suffix is an obvious non-null constant stringAXM1054for awaitedAssert.ThrowsAsync<TException>(...), including non-null constantparamName+Func<Task>overloads and appending.Thrownwhen the exception is usedAXM1055for awaitedAssert.ThrowsAnyAsync<TException>(...), appending.Thrownwhen the exception is used
The migration support is intentionally narrow and high-confidence. It only offers diagnostics and code fixes for xUnit assertion shapes that map cleanly to Axiom's fluent API without changing value flow or subtle overload semantics.
Before:
Assert.Equal(expected, actual);
Assert.Equal(42, 42, EqualityComparer<int>.Default);
Assert.True(condition);
Assert.Empty(values);
Assert.Contains(expected, values);
Assert.Contains("sub", actual);
Assert.Contains("sub", actual, StringComparison.OrdinalIgnoreCase);
Assert.StartsWith("pre", actual, StringComparison.OrdinalIgnoreCase);
Assert.EndsWith("suf", actual, StringComparison.OrdinalIgnoreCase);
Assert.Contains(key, lookup);
var found = Assert.Contains(key, lookup);
var item = Assert.Single(values);
var match = Assert.Single(values, value => value > 0);
Assert.Throws<InvalidOperationException>(() => work());
var ex = Assert.Throws<ArgumentNullException>("name", () => work());
After:
actual.Should().Be(expected);
42.Should().Be(42, EqualityComparer<int>.Default);
condition.Should().BeTrue();
values.Should().BeEmpty();
values.Should().Contain(expected);
actual.Should().Contain("sub");
actual.Should().Contain("sub", StringComparison.OrdinalIgnoreCase);
actual.Should().StartWith("pre", StringComparison.OrdinalIgnoreCase);
actual.Should().EndWith("suf", StringComparison.OrdinalIgnoreCase);
lookup.Should().ContainKey(key);
var found = lookup.Should().ContainKey(key).WhoseValue;
var item = values.Should().ContainSingle().SingleItem;
var match = values.Should().ContainSingle(value => value > 0).SingleItem;
new Action(() => work()).Should().Throw<InvalidOperationException>();
var ex = new Action(() => work()).Should().Throw<ArgumentNullException>().WithParamName("name").Thrown;
The migration suggestions use semantic matching, so they only target xUnit's real Assert API. They do not flag custom helper classes named Assert.
The dictionary-key rules follow Axiom's ContainKey and NotContainKey receiver shape. They support IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>, Dictionary<TKey, TValue>, ReadOnlyDictionary<TKey, TValue>, ConcurrentDictionary<TKey, TValue>, and ImmutableDictionary<TKey, TValue>.
They also intentionally skip shapes that are not obviously semantics-preserving yet, including:
- precision, inspectors, and user-message overloads
- string-equality overloads that rely on xUnit's dedicated string semantics such as
Assert.Equal(..., ignoreCase: ...) - comparer-bearing equality overloads that would need to land on Axiom's specialized string assertion surface rather than a direct local-comparer value assertion
Assert.StartsWith(...)andAssert.EndsWith(...)overloads that useMemory<char>orSpan<char>Assert.StartsWith(...)andAssert.EndsWith(...)when the expected prefix or suffix is not an obvious non-null constant string- nongeneric
Assert.Single(subject)calls when the returned item is used Assert.Throws<TException>(...)consumed-result shapes outside thestring? paramName, Action testCodeoverloadAssert.Throws<TException>(paramName, ...)whenparamNameis not an obvious non-null constant string- non-awaited
Assert.ThrowsAsync<TException>(...)andAssert.ThrowsAnyAsync<TException>(...)shapes Assert.ThrowsAsync<TException>(paramName, ...)whenparamNameis not an obvious non-null constant string
NUnit Assert Migration Suggestions¶
Rules:
AXM1024forAssert.That(actual, Is.EqualTo(expected))AXM1025forAssert.That(actual, Is.Not.EqualTo(expected))AXM1026forAssert.That(value, Is.Null)AXM1027forAssert.That(value, Is.Not.Null)AXM1028forAssert.That(condition, Is.True)AXM1029forAssert.That(condition, Is.False)AXM1030forAssert.That(collection, Is.Empty)AXM1031forAssert.That(collection, Is.Not.Empty)AXM1040forAssert.That(actual, Does.Contain(expectedSubstring))on string subjectsAXM1041forAssert.That(actual, Does.Not.Contain(expectedSubstring))on string subjectsAXM1042forAssert.That(actual, Does.StartWith(expectedPrefix))when the prefix is an obvious non-null constant stringAXM1043forAssert.That(actual, Does.EndWith(expectedSuffix))when the suffix is an obvious non-null constant stringAXM1044forAssert.That(collection, Has.Count.EqualTo(expectedCount))AXM1045forAssert.That(actual, Is.SameAs(expected))AXM1046forAssert.That(actual, Is.Not.SameAs(expected))AXM1056forAssert.That(actual, Is.GreaterThan(expected))AXM1057forAssert.That(actual, Is.GreaterThanOrEqualTo(expected))AXM1058forAssert.That(actual, Is.LessThan(expected))AXM1059forAssert.That(actual, Is.LessThanOrEqualTo(expected))AXM1060forAssert.That(actual, Is.InRange(minimum, maximum))AXM1061forAssert.That(actual, Is.TypeOf<TExpected>())AXM1062forAssert.That(actual, Is.InstanceOf<TExpected>())AXM1063forAssert.That(actual, Is.AssignableTo<TExpected>())AXM1064forAssert.That(actual, Is.Not.InstanceOf<TExpected>())AXM1065forAssert.That(actual, Is.Not.AssignableTo<TExpected>())AXM1066forAssert.ThrowsAsync<TException>(...)in async contexts, appending.Thrownwhen the returned exception is usedAXM1067forAssert.CatchAsync<TException>(...)in async contexts, appending.Thrownwhen the returned exception is used
The NUnit migration support is still intentionally narrow. It covers Does.*, Has.Count.EqualTo(...), ordered value, range, reference identity, generic type constraints, and async exception assertions that map directly onto the current Axiom surface without guessing through richer constraint chains.
Before:
Assert.That(actual, Is.EqualTo(expected));
Assert.That(value, Is.Not.Null);
Assert.That(condition, Is.True);
Assert.That(values, Is.Empty);
Assert.That(actual, Does.Contain("sub"));
Assert.That(actual, Does.Not.Contain("archived"));
Assert.That(actual, Does.StartWith("pre"));
Assert.That(actual, Does.EndWith("suf"));
Assert.That(values, Has.Count.EqualTo(2));
Assert.That(value, Is.SameAs(value));
Assert.That(2, Is.GreaterThan(1));
Assert.That(2, Is.InRange(1, 3));
Assert.That(value, Is.TypeOf<object>());
Assert.That(value, Is.InstanceOf<object>());
Assert.That(value, Is.Not.AssignableTo<string>());
After:
actual.Should().Be(expected);
value.Should().NotBeNull();
condition.Should().BeTrue();
values.Should().BeEmpty();
actual.Should().Contain("sub");
actual.Should().NotContain("archived");
actual.Should().StartWith("pre");
actual.Should().EndWith("suf");
values.Should().HaveCount(2);
value.Should().BeSameAs(value);
2.Should().BeGreaterThan(1);
2.Should().BeInRange(1, 3);
value.Should().BeOfType<object>();
value.Should().BeAssignableTo<object>();
value.Should().NotBeAssignableTo<string>();
These suggestions use semantic matching against NUnit's real APIs. They intentionally skip tolerance/comparer variations, message-bearing overloads, richer Does.* chains, Has.* chains beyond Has.Count.EqualTo(int), runtime Type constraints, Is.Not.TypeOf<T>(), async exception assertions outside an async context, AsyncTestDelegate variable rewrites, and prefix/suffix constraints where the expected value is not an obvious non-null constant string.
NUnit does not expose xUnit's Assert.ThrowsAnyAsync<TException>(...) or Assert.ThrowsAsync<TException>(paramName, ...) shapes. The NUnit derived-exception async assertion shape is Assert.CatchAsync<TException>(...), which maps to Axiom's ThrowAsync<TException>().
MSTest Assert Migration Suggestions¶
Rules:
AXM1032forAssert.AreEqual(expected, actual)AXM1033forAssert.AreNotEqual(expected, actual)AXM1034forAssert.IsNull(value)AXM1035forAssert.IsNotNull(value)AXM1036forAssert.IsTrue(condition)AXM1037forAssert.IsFalse(condition)AXM1038forAssert.AreSame(expected, actual)AXM1039forAssert.AreNotSame(expected, actual)AXM1047forAssert.IsInstanceOfType(value, typeof(T))AXM1048forAssert.IsNotInstanceOfType(value, typeof(T))AXM1049forStringAssert.Contains(actual, expectedSubstring)AXM1050forStringAssert.StartsWith(actual, expectedPrefix)whenexpectedPrefixis an obvious non-null constant stringAXM1051forStringAssert.EndsWith(actual, expectedSuffix)whenexpectedSuffixis an obvious non-null constant stringAXM1052forCollectionAssert.Contains(collection, expected)AXM1053forCollectionAssert.DoesNotContain(collection, unexpected)AXM1068for awaitedAssert.ThrowsExceptionAsync<TException>(...), appending.Thrownwhen the returned exception is usedAXM1069for awaitedAssert.ThrowsExactlyAsync<TException>(...), appending.Thrownwhen the returned exception is usedAXM1070for awaitedAssert.ThrowsAsync<TException>(...), appending.Thrownwhen the returned exception is usedAXM1071forAssert.IsGreaterThan(lowerBound, value)AXM1072forAssert.IsGreaterThanOrEqualTo(lowerBound, value)AXM1073forAssert.IsLessThan(upperBound, value)AXM1074forAssert.IsLessThanOrEqualTo(upperBound, value)AXM1075forAssert.IsInRange(minValue, maxValue, value)
MSTest migrations only cover Assert, StringAssert, and CollectionAssert shapes that map directly to Axiom without carrying extra message, comparer, precision, or structural-comparison semantics across.
Before:
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.AreEqual(expected, actual);
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsNull(value);
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsFalse(condition);
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsInstanceOfType(value, typeof(IDisposable));
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsGreaterThan(minimum, count);
Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsInRange(minimum, maximum, count);
StringAssert.Contains(actual, "archived");
CollectionAssert.DoesNotContain(values, "blocked");
After:
actual.Should().Be(expected);
value.Should().BeNull();
condition.Should().BeFalse();
value.Should().BeAssignableTo<IDisposable>();
count.Should().BeGreaterThan(minimum);
count.Should().BeInRange(minimum, maximum);
actual.Should().Contain("archived");
values.Should().NotContain("blocked");
Async exception migrations intentionally require an awaited MSTest call. Exact-type MSTest shapes (ThrowsExceptionAsync<TException> and ThrowsExactlyAsync<TException>) map to ThrowExactlyAsync<TException>(); derived-type ThrowsAsync<TException> maps to ThrowAsync<TException>().
MSTest does not expose xUnit's ThrowsAnyAsync<TException> or async paramName assertion shapes. Message-bearing MSTest async exception overloads remain manual migrations.
Ordered-value migrations preserve MSTest's bound-first argument order: Assert.IsGreaterThan(lowerBound, value) becomes value.Should().BeGreaterThan(lowerBound).
These suggestions use semantic matching against MSTest's real Assert, StringAssert, and CollectionAssert APIs. They intentionally skip message-bearing, comparer, precision, structural-comparison, non-comparable ordering, and other richer MSTest assertion families, plus StringAssert.StartsWith(...) and StringAssert.EndsWith(...) when the expected prefix or suffix is not an obvious non-null constant string.
For a broader mapping table and practical migration notes, see Migrating to Axiom.