c# Moq 和异步方法的模拟

来源:这里教程网 时间:2026-02-21 17:37:40 作者:

Moq 模拟 async 方法必须返回 Task 或 Task

Moq 无法直接模拟

async void
方法(也不该这么做),所有被模拟的异步方法签名必须是
Task
Task<t></t>
。如果你看到
NotSupportedException: Cannot setup method with return type Void
,大概率是接口或虚方法声明成了
async void DoSomething()
—— 这属于设计错误,需先改为
Task DoSomething()

常见错误场景:在接口中定义了

void SaveAsync(...)
却期望 Moq 返回可 await 的结果;或者误把同步方法标记为
async
但没改返回类型。

接口方法必须声明为
Task
/
Task<t></t>
,不能是
void
被 mock 的类中对应方法需是
virtual
或实现接口,否则 Moq 无法重写
不要在
Setup
中直接
await
,Moq 的
ReturnsAsync
Returns
是同步配置行为

用 ReturnsAsync 正确模拟 Task 返回值

ReturnsAsync
是 Moq 提供的语法糖,等价于
Returns(Task.FromResult(value))
,专用于简化
Task<t></t>
的模拟。它内部自动包装成已完成的
Task
,不会真正启动异步流程,适合单元测试中快速构造确定性响应。

注意:如果返回的是

null
且泛型参数为引用类型,需显式写
ReturnsAsync((string)null)
,否则 C# 类型推导可能失败。

var mockService = new Mock<IDataService>();
mockService.Setup(x => x.FetchUserAsync(123))
    .ReturnsAsync(new User { Id = 123, Name = "Alice" });
<p>// 测试代码中可正常 await
var user = await mockService.Object.FetchUserAsync(123); // 返回预设对象

模拟 Task(无返回值)用 Returns + Task.CompletedTask

对于声明为

Task DoWorkAsync()
的方法,不能用
ReturnsAsync
(它只接受
T
参数),而应使用
Returns(Task.CompletedTask)
。这是最轻量、最推荐的方式 —— 它返回一个已成功完成的静态
Task
实例,零分配、无调度开销。

别用

Task.Run(() => {})
Task.Delay(0)
替代,它们会触发线程池调度,增加不确定性,还可能干扰测试时序判断。

mockService.Setup(x => x.LogAsync("event"))
    .Returns(Task.CompletedTask);
<p>// 调用后立即完成,不阻塞
await mockService.Object.LogAsync("event"); // 成功返回

需要验证异步执行顺序?小心 SetupSequence 和 await 时机

SetupSequence
可用于模拟多次调用返回不同结果,但它本身不感知 await。如果你在测试中连续 await 同一 mock 方法,要确保每次 await 都拿到预期值 —— 这依赖于调用次数,而非“异步完成时间”。Moq 不模拟真实异步延迟,所以不要指望靠它测“并发竞争”或“超时逻辑”。

真正需要控制异步行为(如延迟、取消、异常)时,应改用

TaskCompletionSource<t></t>
手动构造可控制的
Task
,再传给
Returns

var tcs = new TaskCompletionSource<string>();
mockService.Setup(x => x.LoadConfigAsync()).Returns(tcs.Task);
<p>// 后续在测试中可手动完成:tcs.SetResult("config.json");
// 或取消:tcs.SetException(new OperationCanceledException());

这种写法灵活但复杂,多数场景用

ReturnsAsync
Task.CompletedTask
就够了;一旦开始手动管理
TaskCompletionSource
,就得自己处理线程安全和状态一致性 —— 这往往是被忽略的复杂点。

相关推荐