Description
.Returns() only accepts a synchronous Func<T> for computed/delayed values on an async-returning member — there doesn't appear to be any way to have a mocked async call return a Task<T>/ValueTask<T> that stays genuinely pending and completes only later (e.g. after a real await Task.Delay(...)). This makes it impossible to write a test that races a mocked async call against an external timeout, which is a common pattern for testing timeout-handling logic.
Repro
using TUnit.Mocks;
var mock = ISomeInterface.Mock(); // where SomeMethodAsync() : Task<int>
_ = mock.SomeMethodAsync().Returns(async () =>
{
await Task.Delay(1000);
return 42;
});
error CS4010: Cannot convert async lambda expression to delegate type 'Func<int>'. An async lambda
expression may return void, Task or Task<T>, none of which are convertible to 'Func<int>'.
The .Returns() overload set apparently only has a Func<T> (synchronous) computed-value overload — there is no Func<Task<T>>/Func<ValueTask<T>> overload that would let the returned task itself be a real, still-pending task.
What we tried instead (doesn't work for this use case)
Replacing the async delay with a synchronous blocking delay:
_ = mock.SomeMethodAsync().Returns(() =>
{
Thread.Sleep(1000);
return 42;
});
This compiles, but changes the semantics in a way that breaks timeout-racing tests: production code that does something like await Task.WhenAny(mock.SomeMethodAsync(), Task.Delay(timeout)) never gets a chance to race, because the mocked call itself doesn't return an (incomplete) task until after the full synchronous delay has already elapsed — by the time Task.WhenAny receives the "actual call" task, it's already complete, so the timeout branch can never win regardless of how short timeout is.
Environment
- TUnit.Mocks: 1.61.38 (latest at time of filing)
- LangVersion: preview (C# 14)
Impact / ask
Is there a supported way to configure a mocked async member to return a task that only completes after a real asynchronous delay (so external code racing it against a timeout/cancellation behaves correctly)? If not, could .Returns() gain an overload accepting Func<Task<T>> / Func<ValueTask<T>> used as-is (not further wrapped), so the delay is genuinely asynchronous rather than synchronously blocking the caller?
Context
Found while migrating an OSS health-checks library (dailydevops/healthchecks) from NSubstitute to TUnit.Mocks across ~65 test files. Several of our health checks have a "the underlying client is too slow, so we should time out and report Degraded/Unhealthy" test, and NSubstitute's .Returns(async _ => { await Task.Delay(x); return y; }) pattern was used throughout for this. We ended up keeping NSubstitute for one such test (RabbitMQ's timeout test) specifically because of this gap.
Description
.Returns()only accepts a synchronousFunc<T>for computed/delayed values on an async-returning member — there doesn't appear to be any way to have a mocked async call return aTask<T>/ValueTask<T>that stays genuinely pending and completes only later (e.g. after a realawait Task.Delay(...)). This makes it impossible to write a test that races a mocked async call against an external timeout, which is a common pattern for testing timeout-handling logic.Repro
The
.Returns()overload set apparently only has aFunc<T>(synchronous) computed-value overload — there is noFunc<Task<T>>/Func<ValueTask<T>>overload that would let the returned task itself be a real, still-pending task.What we tried instead (doesn't work for this use case)
Replacing the async delay with a synchronous blocking delay:
This compiles, but changes the semantics in a way that breaks timeout-racing tests: production code that does something like
await Task.WhenAny(mock.SomeMethodAsync(), Task.Delay(timeout))never gets a chance to race, because the mocked call itself doesn't return an (incomplete) task until after the full synchronous delay has already elapsed — by the timeTask.WhenAnyreceives the "actual call" task, it's already complete, so the timeout branch can never win regardless of how shorttimeoutis.Environment
Impact / ask
Is there a supported way to configure a mocked async member to return a task that only completes after a real asynchronous delay (so external code racing it against a timeout/cancellation behaves correctly)? If not, could
.Returns()gain an overload acceptingFunc<Task<T>>/Func<ValueTask<T>>used as-is (not further wrapped), so the delay is genuinely asynchronous rather than synchronously blocking the caller?Context
Found while migrating an OSS health-checks library (dailydevops/healthchecks) from NSubstitute to TUnit.Mocks across ~65 test files. Several of our health checks have a "the underlying client is too slow, so we should time out and report Degraded/Unhealthy" test, and NSubstitute's
.Returns(async _ => { await Task.Delay(x); return y; })pattern was used throughout for this. We ended up keeping NSubstitute for one such test (RabbitMQ's timeout test) specifically because of this gap.