我正在尝试对我正在构建的一个类进行单元测试,该类调用多个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
设置中的某些内容应该归咎于......
答案 0 :(得分:16)
你的问题出现在这个模拟中:
new Task<IEnumerable<MobileContent>>(() => new List<MobileContent>())
您不应在异步代码中使用Task
构造函数。相反,请使用Task.FromResult
:
Task.FromResult<IEnumerable<MobileContent>>(new List<MobileContent>())
我建议您阅读我的MSDN article或async
blog post,其中指出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
单元测试。
答案 1 :(得分:7)
当您使用await
启动其中包含await
的异步方法时,您遇到了典型的死锁问题,然后在启动它之后#39;立即对该任务执行阻止等待(当您在Result
中呼叫DownloadAndStore
时)。
当您致电await
时,它会捕获SynchronizationContext.Current
的值,并确保将await
来电产生的所有续约发布回该同步上下文。
所以你正在开始一项任务,并且正在进行异步操作。为了继续它的继续,它需要同步上下文是&#34; free&#34;在某些时候,它可以处理这种延续。
然后来自调用者的代码(在同一个同步上下文中)正在等待任务。在任务完成之前,它不会放弃它同步上下文,但任务需要同步上下文才能完成。你现在有两个任务在彼此等待;经典的僵局。
这里有几种选择。其中一个理想的解决方案是“一直异步”#34;并且永远不会阻止同步上下文开始。这很可能需要您的测试框架的支持。
另一个选择是确保您的await
来电不会回发到同步上下文。您可以将ConfigureAwait(false)
添加到await
的所有任务中,从而实现此目的。如果您这样做,您需要确保它在您的真实程序中的行为,而不仅仅是您的测试框架。如果您的真实框架需要使用捕获同步上下文,那么这不是一个选项。
您还可以创建自己的消息泵,它具有自己的同步上下文,您可以在每个测试的范围内使用它。这允许测试本身阻塞,直到所有异步操作完成,但允许该消息泵内的所有内容完全异步。