Skip to content

[TUnit.Mocks] No way to make an async member's .Returns() genuinely pending (only synchronous Func<T> supported) - breaks timeout-race tests #6495

Description

@samtrion

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions