[英]Unit testing async method - test never completes
我正在尝试单元测试我正在构建的一个类,它调用了许多URL(异步)并检索内容。
这是我遇到问题的测试:
[Test]
public void downloads_content_for_each_url()
{
_mockGetContentUrls.Setup(x => x.GetAll())
.Returns(new[] { "http://www.url1.com", "http://www.url2.com" });
_mockDownloadContent.Setup(x => x.DownloadContentFromUrlAsync(It.IsAny<string>()))
.Returns(new Task<IEnumerable<MobileContent>>(() => new List<MobileContent>()));
var downloadAndStoreContent= new DownloadAndStoreContent(
_mockGetContentUrls.Object, _mockDownloadContent.Object);
downloadAndStoreContent.DownloadAndStore();
_mockDownloadContent.Verify(x => x.DownloadContentFromUrlAsync("http://www.url1.com"));
_mockDownloadContent.Verify(x => x.DownloadContentFromUrlAsync("http://www.url2.com"));
}
DownloadContent
的相关部分是:
public void DownloadAndStore()
{
//service passed in through ctor
var urls = _getContentUrls.GetAll();
var content = DownloadAll(urls)
.Result;
//do stuff with content here
}
private async Task<IEnumerable<MobileContent>> DownloadAll(IEnumerable<string> urls)
{
var list = new List<MobileContent>();
foreach (var url in urls)
{
var content = await _downloadMobileContent.DownloadContentFromUrlAsync(url);
list.AddRange(content);
}
return list;
}
当我的测试运行时,它永远不会完成 - 它只是挂起。
我怀疑我的_mockDownloadContent
设置中的一些事情应该归咎于......
你的问题在于这个模拟:
new Task<IEnumerable<MobileContent>>(() => new List<MobileContent>())
您不应该在异步代码中使用Task
构造函数。 相反,使用Task.FromResult
:
Task.FromResult<IEnumerable<MobileContent>>(new List<MobileContent>())
我建议您阅读我的MSDN文章或async
博客文章 ,其中指出不应将Task
构造函数用于async
代码。
此外,我建议您采取Servy的建议,并“一直”异步(这也包含在我的MSDN文章中)。 如果正确使用await
,您的代码将更改为如下所示:
public async Task DownloadAndStoreAsync()
{
//service passed in through ctor
var urls = _getContentUrls.GetAll();
var content = await DownloadAllAsync(urls);
//do stuff with content here
}
你的测试看起来像:
[Test]
public async Task downloads_content_for_each_url()
{
_mockGetContentUrls.Setup(x => x.GetAll())
.Returns(new[] { "http://www.url1.com", "http://www.url2.com" });
_mockDownloadContent.Setup(x => x.DownloadContentFromUrlAsync(It.IsAny<string>()))
.Returns(Task.FromResult<IEnumerable<MobileContent>>(new List<MobileContent>()));
var downloadAndStoreContent= new DownloadAndStoreContent(
_mockGetContentUrls.Object, _mockDownloadContent.Object);
await downloadAndStoreContent.DownloadAndStoreAsync();
_mockDownloadContent.Verify(x => x.DownloadContentFromUrlAsync("http://www.url1.com"));
_mockDownloadContent.Verify(x => x.DownloadContentFromUrlAsync("http://www.url2.com"));
}
请注意,NUnit的现代版本了解async Task
单元测试没有任何问题。
当你使用await
启动一个有await
的异步方法时,你遇到了经典的死锁问题,然后在启动之后你就会立即对该任务进行阻塞等待(当你调用Result
在DownloadAndStore
)。
当您调用await
,它将捕获SynchronizationContext.Current
的值,并确保将await
调用产生的所有延续都发回到该同步上下文。
所以你正在开始一项任务,它正在进行异步操作。 为了使它继续延续,它需要同步上下文在某个时刻“自由”,以便它可以处理该延续。
然后是来自调用者的代码(在同一个同步上下文中)等待任务。 在任务完成之前,它不会放弃它对同步上下文的保持,但是任务需要同步上下文可以自由完成。 你现在有两个任务在彼此等待; 经典的僵局。
这里有几种选择。 其中一个理想的解决方案是“一直异步”并且永远不会阻止同步上下文。 这很可能需要您的测试框架的支持。
另一种选择是确保您的await
调用不会回发到同步上下文。 您可以通过将ConfigureAwait(false)
添加到您await
所有任务来完成此操作。 如果你这样做,你需要确保它在你的真实程序中也是你想要的行为,而不仅仅是你的测试框架。 如果您的真实框架需要使用捕获同步上下文,那么这不是一个选项。
您还可以使用自己的同步上下文创建自己的消息泵,并在每个测试的范围内使用它。 这允许测试本身阻塞,直到所有异步操作完成,但允许该消息泵内的所有内容完全异步。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.