NUnit测试中FromResult创建的Task的ContinueWith未执行?如何修复?
问题分析与修复方案
这个问题我之前也踩过坑,本质是异步操作的调度时机在搞鬼——咱们一步步拆解:
为什么测试会失败?
你说得没错,Task.FromResult确实返回了一个已经完成的任务,但ContinueWith的回调并不是一定会在调用GetStringAsync的线程上同步执行。它的执行时机由TaskScheduler(任务调度器)决定:
- 默认情况下,即使前置任务已完成,
ContinueWith可能会把回调调度到线程池线程,或者等待当前同步上下文空闲后再执行。 - 你的测试代码在调用
d.Download()之后立刻执行Assert,这时候ContinueWith的回调还没来得及被调度执行,content还是初始的空字符串,断言自然失败。
另外,原Download方法的设计本身就有缺陷:它启动了异步操作,但没有给调用者任何等待操作完成的机制(比如返回Task),这种“火了就不管”的异步写法在实际项目里也很容易引发时序问题。
修复方案
方案1:改造Download为异步方法(强烈推荐)
这是最合理的长期解决方案,让异步操作的生命周期清晰可控:
修改Downloader类:
public interface IHttpClient { Task<string> GetStringAsync(string url); } class Downloader { public string content = ""; // 改为异步方法,返回Task让调用者可以等待 public async Task DownloadAsync(IHttpClient client) { // 用await替代ContinueWith,代码更简洁,也能正确处理异步时序 content = await client.GetStringAsync("http://someurl"); // 其他异步操作也用await来等待完成 } }
对应的测试代码也改成异步等待:
[Test] public async Task TestDownloadAsync() { var httpMock = new Mock<IHttpClient>(); httpMock.Setup(p => p.GetStringAsync("http://someurl")) .Returns(Task.FromResult("SomeContent")); Downloader d = new Downloader(); // 等待异步操作完全完成后再断言 await d.DownloadAsync(httpMock.Object); Assert.AreEqual("SomeContent", d.content); }
方案2:兼容旧代码(无法修改Download方法时)
如果因为历史原因不能修改原Download方法,我们可以在测试里强制让ContinueWith的回调同步执行,或者等待回调完成:
方式A:Mock时指定同步调度器
[Test] public void TestContinueWith() { var httpMock = new Mock<IHttpClient>(); httpMock.Setup(p => p.GetStringAsync("http://someurl")) .Returns(() => { var completedTask = Task.FromResult("SomeContent"); // 强制ContinueWith在当前测试线程同步执行 completedTask.ContinueWith(_ => {}, TaskScheduler.Current); return completedTask; }); Downloader d = new Downloader(); d.Download(httpMock.Object); Assert.AreEqual("SomeContent", d.content); }
方式B:利用同步上下文捕获回调
[Test] public void TestContinueWith() { var httpMock = new Mock<IHttpClient>(); httpMock.Setup(p => p.GetStringAsync("http://someurl")).Returns(Task.FromResult("SomeContent")); Downloader d = new Downloader(); // 设置同步上下文,确保回调在测试线程执行 var syncContext = new SynchronizationContext(); SynchronizationContext.SetSynchronizationContext(syncContext); d.Download(httpMock.Object); // 触发所有待执行的回调 syncContext.Send(_ => {}, null); Assert.AreEqual("SomeContent", d.content); }
注意:方式B比“等待固定时间”(比如
Task.Delay(100).Wait())更可靠,不会因为机器性能波动导致测试不稳定。
内容的提问来源于stack exchange,提问作者Nathan
相关产品推荐
相关产品推荐

