Asynchronous expectations
Namespaces used on this page
using System.Text;
using aweXpect.Core;
using aweXpect.Core.Constraints;
using aweXpect.Core.EvaluationContext;
using aweXpect.Options;
using aweXpect.Results;
Asynchronous constraints
An IAsyncConstraint<T> receives the CancellationToken of the expectation, which is canceled when the timeout
(WithTimeout) elapses or the caller cancels (WithCancellation). Pass it on to the asynchronous work:
public async ValueTask<ConstraintResult> IsMetBy(Track? actual, CancellationToken cancellationToken)
{
Actual = actual;
if (actual is not null)
{
Outcome = await IsInCatalogAsync(actual, cancellationToken) ? Outcome.Success : Outcome.Failure;
}
return this;
}
An OperationCanceledException from that token needs no handling: a timeout fails the expectation with "did not
finish within …", and a cancellation by the caller leaves it inconclusive.
Stopping without an exception
- A constraint that stops at the cancellation without throwing leaves its
Outcomeundecided. A timeout then fails the expectation in the same way. After a cancellation, the helper classes write the result text withAppendUndecidedResult, which you can override. By default it writes "it could not be verified, because the evaluation was already canceled". - Leave the
Outcomeundecided only for a cancellation. An expectation whose outcome stays undecided although nothing was canceled, e.g. because one branch ofIsMetBydoes not set it or a collection constraint does not callComplete(), fails with "it could not be verified, because the expectation did not decide its outcome".
Repeated checks
An expectation that returns a RepeatedCheckResult<TType, TThat> lets the caller wait for a condition with
.Within(timeout) and .CheckEvery(interval), like Satisfies does. Pass the same RepeatedCheckOptions to the
result and to the constraint, and make the check with CheckRepeatedly, which honours the options. For a player that
starts playing asynchronously:
public class Player
{
public bool IsPlaying { get; private set; }
public void Play(string title) => IsPlaying = true;
}
public static RepeatedCheckResult<Player, IThat<Player?>> IsPlaying(this IThat<Player?> subject)
{
RepeatedCheckOptions options = new();
return new RepeatedCheckResult<Player, IThat<Player?>>(subject.Get().ExpectationBuilder
.AddConstraint((it, grammars) => new IsPlayingConstraint(it, grammars, options)),
subject,
options);
}
private sealed class IsPlayingConstraint(
string it,
ExpectationGrammars grammars,
RepeatedCheckOptions options)
: ConstraintResult.WithNotNullValue<Player>(it, grammars),
IAsyncContextConstraint<Player?>
{
public async ValueTask<ConstraintResult> IsMetBy(Player? actual, IEvaluationContext context,
CancellationToken cancellationToken)
{
Actual = actual;
if (actual is not null)
{
Outcome outcome = await options.CheckRepeatedly(_ =>
{
bool isPlaying = actual.IsPlaying;
Outcome = isPlaying ? Outcome.Success : Outcome.Failure;
return new ValueTask<bool>(isPlaying != IsNegated);
}, context);
if (outcome == Outcome.Undecided)
{
Outcome = Outcome.Undecided;
}
}
return this;
}
protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("is playing").Append(options);
protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append(It).Append(" was not playing");
protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append("is not playing").Append(options);
protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null)
=> stringBuilder.Append(It).Append(" was playing");
}
Player player = new();
_ = Task.Delay(TimeSpan.FromSeconds(1)).ContinueWith(_ => player.Play("Let It Be"));
await Expect.That(player).IsPlaying().Within(TimeSpan.FromSeconds(5)).CheckEvery(TimeSpan.FromMilliseconds(100));
CheckRepeatedlymakes the first check immediately. WhenIsRepeatedistrue, becauseWithinset a positive or an infinite timeout, it repeats the check in the interval until it succeeds, and makes the last check at the timeout.- The check returns whether the expectation is met, so for a negated variant created with
.Invert()it returnstruewhen the player is not playing. The helper class inverts the storedOutcomeitself. - Appending the options writes " within …" to the expectation text when
Withinwas specified.
Contexts, exceptions and cancellation
- The check receives the
IEvaluationContextto use. The first check gets the context of the evaluation, every further check a new one, so that a collection taken withUseMaterializedEnumerableis read again instead of replaying the items that a previous check read. - An exception of code of the caller called through
UserCode.Invokecounts as not met while the check is repeated. When the last check still throws, the expectation fails with that exception. CheckRepeatedlyreturns theOutcome:Successwhen a check succeeded,Failurewhen the last check failed, andUndecidedwhen the evaluation was canceled before the timeout. The cancellation is only observed while waiting for the next check, so the first check is made even when the evaluation is already canceled.- A cancellation at the timeout, or by an effective timeout of the evaluation (
WithTimeoutorTestCancellation.FromTimeout) that is not shorter thanWithinwhen the checks started with the evaluation, lets the last check decide. ForUndecided, the constraint sets itsOutcometoUndecided: the helper class then reports that the expectation could not be verified, and a shorter effective timeout is reported as "did not finish within …".
Synchronous boundaries
Some code has to answer synchronously although it calls asynchronous code, e.g. an IEnumerable<T> that filters its
items with an asynchronous predicate. Wait there with SynchronousBoundary.Wait from aweXpect.Core.Extending
instead of .GetAwaiter().GetResult():
public IEnumerator<Track> GetEnumerator()
=> _tracks.Where(track => SynchronousBoundary.Wait(() => IsInCatalogAsync(track))).GetEnumerator();
The expectations of aweXpect run on the synchronization context of the caller, e.g. on the UI thread of a WPF or
WinForms test. Blocking that thread would deadlock as soon as the asynchronous code continues on it, so Wait starts
the operation without the synchronization context and the task scheduler of the caller.