Moq模拟async方法必须返回Task或Task,不可用async void;接口和虚方法需声明为Task/Task,Setup中用ReturnsAsync模拟Task、Returns(Task.CompletedTask)模拟无返回值Task。

Moq 模拟 async 方法必须返回 Task 或 Task
Moq 无法直接模拟 async void 方法(也不该这么做),所有被模拟的异步方法签名必须是 Task 或 Task。如果你看到 NotSupportedException: Cannot setup method with return type Void,大概率是接口或虚方法声明成了 async void DoSomething() —— 这属于设计错误,需先改为 Task DoSomething()。
常见错误场景:在接口中定义了 void SaveAsync(...) 却期望 Moq 返回可 await 的结果;或者误把同步方法标记为 async 但没改返回类型。
- 接口方法必须声明为
Task/Task,不能是void - 被 mock 的类中对应方法需是
virtual或实现接口,否则 Moq 无法重写 - 不要在
Setup中直接await,Moq 的ReturnsAsync和Returns是同步配置行为
用 ReturnsAsync 正确模拟 Task 返回值
ReturnsAsync 是 Moq 提供的语法糖,等价于 Returns(Task.FromResult(value)),专用于简化 Task 的模拟。它内部自动包装成已完成的 Task,不会真正启动异步流程,适合单元测试中快速构造确定性响应。
注意:如果返回的是 null 且泛型参数为引用类型,需显式写 ReturnsAsync((string)null),否则 C# 类型推导可能失败。
var mockService = new Mock(); mockService.Setup(x => x.FetchUserAsync(123)) .ReturnsAsync(new User { Id = 123, Name = "Alice" }); // 测试代码中可正常 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);
// 调用后立即完成,不阻塞
await mockService.Object.LogAsync("event"); // 成功返回
需要验证异步执行顺序?小心 SetupSequence 和 await 时机
SetupSequence 可用于模拟多次调用返回不同结果,但它本身不感知 await。如果你在测试中连续 await 同一 mock 方法,要确保每次 await 都拿到预期值 —— 这依赖于调用次数,而非“异步完成时间”。Moq 不模拟真实异步延迟,所以不要指望靠它测“并发竞争”或“超时逻辑”。
真正需要控制异步行为(如延迟、取消、异常)时,应改用 TaskCompletionSource 手动构造可控制的 Task,再传给 Returns:
var tcs = new TaskCompletionSource(); mockService.Setup(x => x.LoadConfigAsync()).Returns(tcs.Task); // 后续在测试中可手动完成:tcs.SetResult("config.json"); // 或取消:tcs.SetException(new OperationCanceledException());
这种写法灵活但复杂,多数场景用 ReturnsAsync 和 Task.CompletedTask 就够了;一旦开始手动管理 TaskCompletionSource,就得自己处理线程安全和状态一致性 —— 这往往是被忽略的复杂点。










