Delegates
Describes the possible expectations for delegates and exceptions.
A delegate can be any of the following:
ActionorAction<CancellationToken>
a synchronous method without return value (optionally accepting aCancellationTokenfor timeout)Func<Task>orFunc<CancellationToken, Task>
an asynchronous method without return value (optionally accepting aCancellationTokenfor timeout)Func<ValueTask>orFunc<CancellationToken, ValueTask>
an asynchronous method usingValueTaskwithout return value (optionally accepting aCancellationTokenfor timeout)Func<T>orFunc<CancellationToken, T>
a synchronous method with return valueT(optionally accepting aCancellationTokenfor timeout)Func<Task<T>>orFunc<CancellationToken, Task<T>>
an asynchronous method with return valueT(optionally accepting aCancellationTokenfor timeout)Func<ValueTask<T>>orFunc<CancellationToken, ValueTask<T>>
an asynchronous method usingValueTaskwith return valueT(optionally accepting aCancellationTokenfor timeout)
Not throw
You can verify that the delegate does not throw any exception:
void Act() => {};
await Expect.That(Act).DoesNotThrow();
Throw exception
You can verify that the delegate throws an exception:
void Act() => throw new CustomException("my exception");
await Expect.That(Act).Throws();
Specific exception
You can verify that the delegate throws a specific exception:
void Act() => throw new CustomException("my exception");
await Expect.That(Act).Throws<CustomException>();
await Expect.That(Act).Throws(typeof(CustomException));
This will verify that the thrown exception is of type CustomException or any derived type.
Exact exception
You can verify that the delegate throws exactly a specific exception:
void Act() => throw new CustomException("my exception");
await Expect.That(Act).ThrowsExactly<CustomException>();
await Expect.That(Act).ThrowsExactly(typeof(CustomException));
This will verify that the thrown exception is of type CustomException and not any derived type.
Conditional throw
You can verify that the delegate throws an exception only if a predicate is satisfied (otherwise it verifies, that no exception is thrown):
void Act() => throw new CustomException("my exception");
bool expectThrownException = true;
await Expect.That(Act).Throws<CustomException>().OnlyIf(expectThrownException);
This is especially useful with parametrized tests where it depends on a parameter if an exception is thrown or not.
Exception message
You can verify the message of the thrown exception:
void Act() => throw new CustomException("This is my exception text");
await Expect.That(Act).Throws().WithMessage("This is my exception text");
await Expect.That(Act).Throws().WithMessage().NotEqualTo("some other text");
await Expect.That(Act).Throws().WithMessage().Containing("my exception");
await Expect.That(Act).Throws().WithMessage().NotContaining("something else");
await Expect.That(Act).Throws().WithMessage().StartingWith("This is");
await Expect.That(Act).Throws().WithMessage().NotStartingWith("That was");
await Expect.That(Act).Throws().WithMessage().EndingWith("exception text");
await Expect.That(Act).Throws().WithMessage().NotEndingWith("something else");
WithMessage(expected) is the shorthand for WithMessage().EqualTo(expected).
Only EqualTo and NotEqualTo accept null; the other comparisons reject null and the empty string, because
neither is a substring anything could meaningfully be checked against.
You can use the same configuration options as when comparing strings.
Inner exceptions
You can verify the inner exception of the thrown exception;
void Act() => throw new CustomException("outer", new CustomException("inner"));
await Expect.That(Act).Throws().WithInnerException();
await Expect.That(Act).Throws().WithInner<CustomException>();
Recursive inner exceptions
You can recursively verify the collection of inner exceptions of the thrown exception:
void Act() => throw new AggregateException("outer", new CustomException("inner"));
await Expect.That(Act).Throws().WithRecursiveInnerExceptions(innerExceptions => innerExceptions.HasAtLeast(1).Be<CustomException>());
Other members
You can recursively verify additional members of the exception:
var exception = new CustomException("outer", hResult: 12345);
void Act() => throw exception;
await Expect.That(Act).Throws().WithHResult(12345)
.Because("you can verify the `HResult`");
await Expect.That(Act).Throws()
.Whose(e => e.HResult, h => h.IsGreaterThan(12340))
.Because("you can verify arbitrary additional members");
await Expect.That(Act).Throws()
.Which.IsSameAs(exception)
.Because("you can access the thrown exception");
The ParamName of an ArgumentException continues like the message:
void Act() => throw new ArgumentNullException("myParameter");
await Expect.That(Act).Throws<ArgumentNullException>().WithParamName("myParameter");
await Expect.That(Act).Throws<ArgumentNullException>().WithParamName().NotEqualTo("otherParameter");
await Expect.That(Act).Throws<ArgumentNullException>().WithParamName().Containing("Parameter");
await Expect.That(Act).Throws<ArgumentNullException>().WithParamName().StartingWith("my");
await Expect.That(Act).Throws<ArgumentNullException>().WithParamName().EndingWith("Parameter");
WithParamName(expected) is the shorthand for WithParamName().EqualTo(expected), so a null argument requires the ParamName to be null as well.
Execution time
You can verify that the execution time of a delegate:
await Expect.That(Task.Delay(200)).ExecutesIn().AtMost(300.Milliseconds())
.Because("the delegate should execute faster than 300ms");
await Expect.That(Task.Delay(200)).ExecutesIn().AtLeast(100.Milliseconds())
.Because("the delegate should execute slower than 100ms");
await Expect.That(Task.Delay(200)).ExecutesIn(200.Milliseconds()).Within(50.Milliseconds())
.Because("the delegate should execute within 200ms ± 50ms");
await Expect.That(Task.Delay(200)).ExecutesIn().Between(100.Milliseconds()).And(300.Milliseconds())
.Because("the delegate should execute slower than 100ms and faster than 300ms");
Execute within
There is also a shorthand expectation for a delegate that finishes the execution without throwing an exception in (at most) a given time:
await Expect.That(Task.Delay(200)).ExecutesWithin(TimeSpan.FromMilliseconds(300))
.Because("it should only take about 200ms");
await Expect.That(Task.Delay(200)).DoesNotExecuteWithin(TimeSpan.FromMilliseconds(100))
.Because("it should take at least 200ms");
Eventually
Some values only become correct after a short delay, e.g. because a background task is still running.
Instead of waiting for a fixed amount of time, you can use Eventually() to re-evaluate the delegate
until the expectations are met:
await Expect.That(() => sut.MyProp).Eventually().IsGreaterThan(5);
Because only the subject is re-evaluated, all expectations work as usual, including And, Or and Because:
await Expect.That(() => sut.Name).Eventually().IsNotNull().And.StartsWith("foo");
The delegate is re-evaluated every
DefaultCheckInterval (defaults to 100ms) until the timeout
configured in DefaultEventuallyTimeout (defaults to 30s)
expires. The last wait is shortened so that it never exceeds the timeout, which means that an interval
that is longer than the timeout results in exactly two evaluations. You can overwrite the timeout per
expectation with WithTimeout:
await Expect.That(() => sut.MyProp).Eventually().IsGreaterThan(5).WithTimeout(5.Seconds());
// using aweXpect.Chronology
WithTimeout(Timeout.InfiniteTimeSpan) retries until the expectations are met or the expectation is
cancelled.
An exception thrown by the delegate counts as an unmet expectation and is retried. When the timeout expires
while the delegate is still throwing, the expectation fails and the last exception is reported as the cause
of the failure. If the expectation is cancelled before the timeout expires - via WithCancellation or via
the global TestCancellation setting - it is reported as inconclusive instead of failed. As everywhere else,
an explicit WithTimeout takes precedence over a global TestCancellation timeout.
The timeout bounds how long the delegate is retried, not how long a single evaluation may take: it is only checked between evaluations, so a delegate that blocks for longer than the timeout still runs to completion.
In addition to Func<T>, the asynchronous variant Func<Task<T>> is supported, and on .NET 8 or later also
Func<ValueTask<T>>; each of them also accepts a CancellationToken. As for Expect.That, an async lambda
is ambiguous between the Task and the ValueTask overload, so on .NET 8 or later its type must be stated
explicitly:
Func<Task<int>> subject = async () => await sut.GetCountAsync();
await Expect.That(subject).Eventually().IsGreaterThan(5);