C# 单元测试异步方法-测试从未完成

C# 单元测试异步方法-测试从未完成,c#,asynchronous,async-await,C#,Asynchronous,Async Await,我试图对我正在构建的一个类进行单元测试,该类调用许多URL(异步)并检索内容 以下是我遇到的问题: [Test] public void downloads_content_for_each_url() { _mockGetContentUrls.Setup(x => x.GetAll()) .Returns(new[] { "http://www.url1.com", "http://www.url2.com" }); _mockDownloadCont

我试图对我正在构建的一个类进行单元测试,该类调用许多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"));
}
当我的测试运行时,它永远不会完成——它只是挂起


我怀疑我的
\u mockDownloadContent
的设置中有什么原因…

在使用
await
时遇到了典型的死锁问题,在该方法中,您启动了一个包含
await
的异步方法,然后在启动它之后,您立即对该任务执行阻塞等待(当您在
下载和存储
中调用
结果
时)

当您调用
await
时,它将捕获
SynchronizationContext.Current
的值,并确保由
await
调用产生的所有延续都发回该同步上下文

因此,您正在启动一个任务,它正在执行一个异步操作。为了让它继续它的继续,它需要同步上下文在某个点处于“空闲”状态,以便它可以处理该继续

然后是调用方(在同一同步上下文中)正在等待任务的代码。在任务完成之前,调用方不会放弃对该同步上下文的保留,但任务需要同步上下文空闲才能完成。现在有两个任务彼此等待;典型的死锁

这里有几个选项。其中一个是理想的解决方案,即“一路异步”,并且从一开始就不阻止同步上下文。这很可能需要测试框架的支持

另一个选项是确保您的
await
调用不会发回同步上下文。您可以通过添加
ConfigureAwait(false)来完成此操作
添加到您等待的所有任务中。如果您这样做,您需要确保这也是您在实际程序中想要的行为,而不仅仅是您的测试框架。如果您的实际框架需要使用捕获同步上下文,则这不是一个选项


您还可以使用自己的同步上下文创建自己的消息泵,在每个测试的范围内使用。这允许测试本身阻塞,直到所有异步操作完成,但允许该消息泵内的所有内容完全异步。

您的问题在于此模拟:

new Task<IEnumerable<MobileContent>>(() => new List<MobileContent>())
我建议您阅读我的or,其中指出
任务
构造函数不应用于
异步
代码

此外,我建议您采纳Servy的建议,“一路”执行异步(我的MSDN文章也介绍了这一点)。如果您正确使用
wait
,您的代码将更改为如下所示:

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"));
}
[测试]
公共异步任务为每个url()下载内容
{
_mockGetContentUrls.Setup(x=>x.GetAll())
.Returns(新[]{”http://www.url1.com", "http://www.url2.com" });
_mockDownloadContent.Setup(x=>x.DownloadContentFromUrlAsync(It.IsAny()))
.Returns(Task.FromResult(new List());
var downloadAndStoreContent=新的downloadAndStoreContent(
_mockGetContentUrls.Object,\u mockDownloadContent.Object);
等待downloadAndStoreContent.DownloadAndStoreAsync();
_mockDownloadContent.Verify(x=>x.DownloadContentFromUrlAsync(“http://www.url1.com"));
_mockDownloadContent.Verify(x=>x.DownloadContentFromUrlAsync(“http://www.url2.com"));
}

注意,NUnit的现代版本理解
async Task
单元测试没有任何问题。

挂起时它在哪里?还有
SynchronizationContext。当前的
null,或者您的测试框架提供了上下文吗?旁注,
DownloadAll
真的想一次下载所有URL吗我?你不想把它们并行化,让它们同时运行吗?@Servy是的,这正是我想做的…遵循这个想法,所以你想一次下载一个?因为这就是你正在做的。想想看,这可能不是最好的方式…@Alex Stephen Cleary知道。+1没有si一点讽刺的痕迹,那是非常有用的,并且为我节省了无数浪费的时间。幸运的是,我已经按照servy的建议编写了代码,但我使用的是
任务
构造函数。@斯蒂芬·克利里:如果你发现你在asp.net框架中分享的关于使用异步的见解是不可或缺的,我不能赞扬你够了。保持良好的工作伙伴关系!@Stephen Cleary如果你正在编写这样的单元测试,并且你没有“一路异步”,也就是说,单元测试不是异步的,你在DownloadAndStoreAsync()上调用.Wait(),是否可能出现死锁?这意味着当运行单元测试时,它是在类似UI上下文的上下文中运行的,这可能会导致死锁,还是与控制台应用程序具有相同的上下文。@Vlad:这取决于您的单元测试框架。除非您使用提供UI的UniversalWindows的MSTest,否则MSTest与控制台应用程序具有相同的上下文-ish context.xUnit始终提供UI ish context.NUnit不提供,除了它的一些助手方法提供。因此有点混乱;请检查
SynchronizationContext.Current
,或者只使用比其他方法更可预测的xUnit。
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"));
}