Skip to main content

Analyzers

The aweXpect package includes analyzers that report common mistakes while you write the test. Errors stop the build, warnings point at code that compiles but probably does not do what you meant. Most rules come with a code fix.

The analyzers and the source generators need Roslyn 4.8 or later, which ships with the .NET 8 SDK and Visual Studio 2022 17.8. An older compiler skips them with warning CS8032.

RuleSeverityReported when
aweXpect0001Erroran expectation is neither awaited nor verified
aweXpect0002ErrorEquals is called on an expectation
aweXpect0003Warninga Has… expectation directly follows Throws
aweXpect0004Erroran expectation for an object is applied to a delegate
aweXpect0005Warningan expectation is awaited in an async void method or lambda
aweXpect0006Warninga collection without a defined order is compared by position
aweXpect0007Errora delegate returns a ValueTask on a target without its overload
aweXpect0008Warningthe value of an .Or combination is used
aweXpect0009Warningthe result of an option method is discarded
aweXpect1001 to aweXpect1004Suppressionnullability warnings after e.g. IsNotNull()
aweXpect2001Warninga type in [assembly: GenerateMetadata(…)] gets no registration
aweXpect2002Warninga test project on .NET 8 or later sets <LangVersion> below 9
aweXpect4001 to aweXpect4004Errora method cannot be turned into a generated expectation
aweXpect4005Errora module initializer of an extension cannot be called

aweXpect0001​

Error

An expectation is neither awaited nor verified, so it is never evaluated and never fails. A code fix is available.

bool value = false;

That(value).IsTrue(); // reported: never evaluated
await That(value).IsTrue(); // fixed: awaited, so it fails

The code fix awaits the expectation and makes the enclosing method, local function or lambda async where needed. It is not offered where that would not compile or would change a signature that other code depends on.

Where the code fix is not offered
  • in a lambda that is not async, such as one converted to an Action
  • in a constructor, a property, an iterator, a lock statement or unsafe code
  • in a function with ref locals or ref struct locals
  • in a method with ref, out, in, pointer or ref struct parameters, or one that returns by reference
  • in a partial or [Conditional] method
  • in a method or local function whose return type would have to change while other code depends on it: an override, a virtual method, an interface implementation (also one that only a derived class declares), or one that is called or used as a method group elsewhere

For a ref struct that cannot be used in an async method, verify the expectation synchronously instead, see When you cannot await.

aweXpect0002​

Error

Equals is called on an expectation. It compares the expectation object itself and does not verify anything.

int subject = 1;

bool isEqual = That(subject).Equals(2); // reported: verifies nothing
await That(subject).IsEqualTo(2); // fixed

aweXpect0003​

Warning

A Has… expectation is used directly after Throws, where the sentence "throws a …" needs the With… vocabulary. Two code fixes are available: switch to the With… twin, or insert .Which.

void Act() => throw new InvalidOperationException("Yesterday");

await That(Act).Throws<InvalidOperationException>().HasMessage("Yesterday"); // reported
await That(Act).Throws<InvalidOperationException>().WithMessage("Yesterday"); // fixed

The Has… vocabulary belongs after .Which. See Delegates.

aweXpect0004​

Error

An expectation for an object is applied to a delegate subject, so it checks the delegate instead of what it returns. A code fix is available for a delegate that returns a value.

int Act() => 3;

await That(Act).IsEqualTo(3); // reported: checks the delegate
await That(Act).DoesNotThrow().WhoseResult.IsEqualTo(3); // fixed: checks the returned value

See Delegates.

aweXpect0005​

Warning

An expectation is awaited in an async void method or lambda, so its failure is thrown after the test has completed and is not observed by the test.

List<int> values = [1, 2, 3];

values.ForEach(async x => await That(x).IsGreaterThan(0)); // reported: async void lambda
foreach (int x in values)
{
await That(x).IsGreaterThan(0); // fixed: awaited by the test
}

Return a Task instead, or await the expectation in the test method itself.

aweXpect0006​

Warning

A collection without a defined order, such as a set or a dictionary, is compared by position. A code fix is available.

HashSet<int> values = [1, 2, 3];

await That(values).IsEqualTo([1, 2, 3]); // reported: depends on the enumeration order
await That(values).IsEqualTo([1, 2, 3]).InAnyOrder(); // fixed

IsEqualTo, Contains and IsContainedIn compare by position unless .InAnyOrder() is appended, which the code fix does. StartsWith, EndsWith and IgnoringInterspersedItems() are about the order itself and have no meaning for such a collection, so .InAnyOrder() does not help there. Sorted collections are not reported. See Collections.

aweXpect0007​

Error

A delegate subject returns a ValueTask on a target without ValueTask delegate overloads, so the ValueTask is never awaited. A code fix is available.

async ValueTask Act() => await Task.Yield();

await That(() => Act()).DoesNotThrow(); // reported on targets before .NET 8
await That(() => Act().AsTask()).DoesNotThrow(); // fixed

On any target, the rule also reports a delegate whose ValueTask is given explicitly as the type argument, as in That<ValueTask>(() => Act()). An explicit Task type argument has the same effect and is reported as well, as in That<Task>(() => ActAsync()): remove the type argument, so that the task is awaited. See Delegates.

Why targets before .NET 8 are affected

These older targets use the .NET Standard 2.0 build of aweXpect, which has no ValueTask delegate overloads.

aweXpect0008​

Warning

The value of an expectation is used although an .Or combines it with an alternative that has no value of this type, so the value can be the default.

object subject = "Yesterday";

int count = await That(subject).Is<string>().Or.Is<int>(); // reported: 0 for a string subject
await That(subject).Is<string>().Or.Is<int>(); // fixed: the value is not used

The value is default (also null for a reference type that is not annotated as nullable) when another alternative was met. Do not use it, or check it first and suppress the warning there. The rule is not reported when the value has the type of the subject, or a base type or an interface of it, because then every alternative returns the subject. See Combining expectations.

aweXpect0009​

Warning

The value returned by an option method of an immutable option, such as a Quantifier or the StringEqualityOptions, is discarded, so the call has no effect.

using aweXpect.Core;
using aweXpect.Options;

OptionsHolder<Quantifier> quantifier = new(Quantifier.Default);

quantifier.Value.AtLeast(2); // reported: the quantifier stays unchanged
quantifier.Value = quantifier.Value.AtLeast(2); // fixed: the holder gets the new quantifier
quantifier.AtLeast(2); // fixed: the holder's option method replaces its value

The options are immutable values, so an option method returns new options and leaves the ones it is called on unchanged. The rule covers every options type marked with [ImmutableOptions]: those of aweXpect, those of the extension packages, e.g. the JsonOptions, also in a lambda with a block body like o => { o.IgnoringMember("id"); return o; }, and those of other extensions that mark their own. A call that is the whole body of a lambda or local function written with => is not reported, as it is usually there to check that the option throws. Authors of extensions find more in Options.

Nullability suppressor​

After an expectation that a null subject can never satisfy, such as IsNotNull(), the aweXpect package suppresses the nullability warnings CS8600, CS8602, CS8604 and CS8629 for that subject, with the suppression IDs aweXpect1001 to aweXpect1004. Expectations of extension packages take part when they are marked with [GuaranteesNotNull], see nullability warnings.

string? title = new Track().Title;

await That(title).IsNotNull();
int length = title.Length; // no CS8602
When the suppression applies

The subject has to be a local variable or a parameter that is neither a ref nor a parameter of a primary constructor, which any member can write to, and the expectation has to be awaited or verified in a preceding statement in the same method, local function or lambda, without a branch, a label or a write to the subject in that statement or in between. Assigning the result of an expectation to its own subject, as in subject = await That(subject).IsNotNull();, does not count as a write, because the result is the subject. A ref alias of the subject prevents the suppression, as it can change the subject unnoticed. Only the warnings are suppressed; the null state of the compiler is unchanged.

Metadata generator​

The source generator warns with aweXpect2001 when a type named in [assembly: GenerateMetadata(typeof(…))] yields no registration and stays on the reflection path. See Native AOT and trimming.

Test framework adapter​

The source generator warns with aweXpect2002 when a test project on .NET 8 or later sets <LangVersion> below 9. The generated test framework adapter registers itself with a module initializer, which needs C# 9, and on .NET 8 or later the loaded assemblies are not scanned for it. Without the adapter, aweXpect throws its own exceptions, so a skipped or inconclusive test is reported as failed. Set <LangVersion> to 9 or later, or register the adapter before the first expectation, see test framework adapter.

TestFrameworkRegistry.Register(new aweXpect.Frameworks.NunitAdapter());

For extension authors​

These rules are reported while you build an extension package on aweXpect.Core.

Expectation generator​

The source generator in aweXpect.Core reports an error for a method marked with [GenerateExpectation] or [GenerateCollectionExpectation] that it cannot turn into an expectation, see generated expectations:

RuleReported when
aweXpect4001the class of the method is not a top-level, non-generic, static partial class or is file-local
aweXpect4002the method, its parameters, the name or the expectation text are not supported, with the reason
aweXpect4003the Result names no static method that returns a string and takes the subject
aweXpect4004the FormattedItem names no static method that returns a value and takes the item

Registration generator​

The source generator in aweXpect.Core reports the error aweXpect4005 when a name in the MSBuild property AweXpectModuleInitializers names no accessible static method without parameters, and leaves the call out of the module initializer. The build targets of an extension set this property, see registration from the test project.