c# Moq 和异步方法的模拟

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

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

Moq 无法直接模拟 async void 方法(也不该这么做),所有被模拟的异步方法签名必须是 TaskTask。如果你看到 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 的 ReturnsAsyncReturns 是同步配置行为

用 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());

这种写法灵活但复杂,多数场景用 ReturnsAsyncTask.CompletedTask 就够了;一旦开始手动管理 TaskCompletionSource,就得自己处理线程安全和状态一致性 —— 这往往是被忽略的复杂点。