Generated expectations
An expectation that only checks a condition on its subject doesn't need a constraint class of its own: mark a
static method that decides the condition with [GenerateExpectation] from aweXpect.Core.Extending, and the source
generator in the aweXpect.Core package writes the extension methods, the constraint and its failure messages. It
runs in every project that references aweXpect.Core, directly or through aweXpect, so an extension package can use it
as well as a test project.
using aweXpect.Core.Extending;
public static partial class ThatTrack
{
[GenerateExpectation("Is{Not}LongerThan", "is {not} longer than {minimum}")]
private static bool IsLongerThan(Track track, TimeSpan minimum)
=> track.Duration > minimum;
}
Track track = new("Love Me Do", new TimeSpan(0, 2, 22));
await Expect.That(track).IsLongerThan(TimeSpan.FromMinutes(2));
await Expect.That(track).IsNotLongerThan(TimeSpan.FromMinutes(3));
The method
-
The method is
staticand returnsbool,Task<bool>orValueTask<bool>, or aPredicateResultinstead of theboolwhen it cannot always answer. Its first parameter is the subject, the other parameters become the parameters of the expectation. -
A
CancellationTokenparameter marked with[EvaluationCancellationToken]receives the token of the evaluation and is no parameter of the expectation. It can be at any position after the subject, e.g. before aparamsparameter. ACancellationTokenparameter that is not marked is a parameter of the expectation like any other.[GenerateExpectation("Is{Not}OneOf", "is {not} one of {titles}")]private static async Task<bool> IsOneOf(Track track,[EvaluationCancellationToken] CancellationToken cancellationToken, params string[] titles)=> titles.Contains(await LookUpTitle(track, cancellationToken)); -
It is declared in a top-level, non-generic,
static partialclass that is notfile-local, which receives the generated extension methods. The method itself can beprivate. -
The generated extension methods are
public, unless the class, the subject or a parameter type isinternal: then they areinternalas well. -
A reference type parameter that is not nullable is checked in the generated method, so
nullthrows anArgumentNullExceptionwhere the caller passes it.
Name and texts
The name is the name of the expectation. A {Not} in it, or a NegatedName, also generates the negated
expectation, e.g. "Is{Not}LongerThan" generates IsLongerThan and IsNotLongerThan.
The expectationText follows the message conventions. {not} is removed from the
expectation and becomes "not" in its negation. Any other placeholder, e.g. {minimum}, names a parameter, whose
formatted value replaces it.
When "Not" and "not" in place of {Not} and {not} don't read well, give the negated name in NegatedName and the
negated text in NegatedExpectationText, with the same placeholders. Every expectation can be negated with
DoesNotComplyWith, even one without a negated name, so the text needs either {not} or a NegatedExpectationText:
[GenerateExpectation("HasTitle", "has title {title}",
NegatedName = "DoesNotHaveTitle", NegatedExpectationText = "does not have title {title}")]
private static bool HasTitle(Track track, string title)
=> track.Title == title;
The result after "but" shows the formatted subject. To describe it differently, name a static method that returns the
text after the subject in Result. It takes the subject, optionally followed by the other parameters:
[GenerateExpectation("Is{Not}LongerThan", "is {not} longer than {minimum}", Result = nameof(DescribeDuration))]
private static bool IsLongerThan(Track track, TimeSpan minimum)
=> track.Duration > minimum;
private static string DescribeDuration(Track track)
=> $"was {track.Duration:m\\:ss} long";
Expected that track
is longer than 3:00,
but it was 2:22 long
To describe the subject itself, the method can end with a string and an ExpectationGrammars parameter. It then
receives how the subject is referred to, e.g. "it", and returns the whole text after "but":
private static string DescribeDuration(Track track, string it, ExpectationGrammars grammars)
=> $"{it}{grammars.SubjectVerb(it, " was ", " were ")}{track.Duration:m\\:ss} long";
The XML documentation is derived from the expectation text. Summary, NegatedSummary, Remarks and
NegatedRemarks replace or extend it with XML documentation of your own.
null subjects
The generated expectations follow the rule for null subjects, and
the type of the subject parameter decides how:
| Subject parameter | Generated expectations |
|---|---|
a reference type, e.g. Track | on IThat<Track?>; null fails the expectation and its negation |
a value type, e.g. TimeSpan | on IThat<TimeSpan> and on IThat<TimeSpan?>, where null fails both |
a nullable type, e.g. Track? or TimeSpan? | on IThat<Track?> or IThat<TimeSpan?>; the method decides about null |
a nullable type with [DisallowNull] | on the nullable type; null fails the expectation and its negation |
Set NullableOverload = false to generate no overload on the nullable value type, e.g. when a separate class of yours
already holds the expectations on it.
An expectation that fails for a null subject is marked with [GuaranteesNotNull].
When the method decides about null, [NotNullWhen] on its subject parameter tells which of the two expectations
guarantees a subject that is not null, like on string.IsNullOrEmpty:
[GenerateExpectation("Is{Not}Untitled", "is {not} untitled")]
private static bool IsUntitled([NotNullWhen(false)] Track? track)
=> track is null || track.Title.Length == 0;
Here IsNotUntitled() fails for a null subject, so the subject cannot be null after it.
Questions that cannot be answered
When the method cannot decide for some subjects, e.g. a track without a duration, it returns a PredicateResult. A
bool converts implicitly, so only that case needs PredicateResult.FailureBothWays with the reason, which follows
"it" in the failure message:
[GenerateExpectation("Is{Not}LongerThan", "is {not} longer than {minimum}")]
private static PredicateResult IsLongerThan(Track track, TimeSpan minimum)
=> track.Duration <= TimeSpan.Zero
? PredicateResult.FailureBothWays("had no duration")
: track.Duration > minimum;
Expected that track
is not longer than 3:00,
but it had no duration
The expectation and its negation fail alike, as negating a question that cannot be answered does not make it true.
The Result method still describes a subject that the method decided about.
Collections
[GenerateCollectionExpectation] generates an expectation on the items of a collection from the same kind of method,
e.g. AreLongerThan for That(tracks).All().AreLongerThan(…). It takes the name, the expectation text on a single
item and its plural:
[GenerateCollectionExpectation("AreLongerThan", "is longer than {minimum}", "are longer than {minimum}")]
private static bool IsLongerThan(Track track, TimeSpan minimum)
=> track.Duration > minimum;
Together with [GenerateExpectation], the text on a single item can be left out, as it is the expectation text of
[GenerateExpectation] without {not}:
[GenerateExpectation("Is{Not}LongerThan", "is {not} longer than {minimum}")]
[GenerateCollectionExpectation("AreLongerThan", "are longer than {minimum}")]
private static bool IsLongerThan(Track track, TimeSpan minimum)
=> track.Duration > minimum;
await Expect.That(tracks).All().AreLongerThan(TimeSpan.FromMinutes(2));
await Expect.That(tracks).AtLeast(2).AreLongerThan(TimeSpan.FromMinutes(2));
await Expect.That(tracks).None().AreLongerThan(TimeSpan.FromMinutes(5));
- The method follows the rules above, and its first parameter is the item.
- The expectation text describes a single item, e.g. "is longer than … for all items". The plural text is used
where the quantifier is the subject, e.g. "has values of which at least 2 are longer than …". The quantifier
carries the negation, so neither text contains
{not}. - The result counts the items, e.g. "but only 1 of 3 were", with "did" instead of "were" when the text does not start with a form of "be". The failure message lists the matching or not matching items and the collection.
- The expectation extends the elements of every kind of collection that can hold the item: an
IEnumerable<T>, anImmutableArray<T>and, on .NET 8 or later, anIAsyncEnumerable<T>. Astringitem extends the elements of the collections of strings. - A
nullitem does not match, unless the item parameter is nullable: then the method decides.[DisallowNull]andNullableOverloadwork like for a subject. - An item that the method cannot answer fails the expectation and its negation, e.g. "but for the item at index 1, it had no duration", unless the items before already determine the outcome.
SummaryandRemarksreplace or extend the XML documentation, which is derived from the plural text.
Diagnostics
A method that the generator cannot turn into an expectation is reported as an error, see the expectation generator.