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

8

我正在尝试对我正在构建的一个调用多个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"));
}
< p > 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的设置有问题...


它挂起时在哪里?此外,SynchronizationContext.Current是空的吗,还是由您的测试框架提供了上下文? - Servy
1
顺便提一下,DownloadAll 真的想一个接一个地下载所有的 URL 吗?你不想将它们并行化,让它们同时运行吗? - Servy
@Servy 是的,那基本上就是我想做的...跟着这个思路 http://msdn.microsoft.com/en-us/library/vstudio/hh696703.aspx - Alex
所以你想一个接一个地下载它们?因为这就是你正在做的事情。 - Servy
考虑一下,那可能不是最好的方法... - Alex
这是因为你编写的测试方式有误,你将DownloadContentFromUrlAsync设置为返回一个新的Task(),但你从未启动该任务,因此它永远无法完成;我猜你在使用Moq。如果是这样,还有一个Callback()函数,在你的设置调用之后立即调用 - 你可以在那里启动你的任务。 - Vlad
2个回答

19

你的问题在这个模拟中:

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单元测试。


3
@Alex Stephen Cleary 知道。 - philologon
2
毫无讽刺意味地说,这真是惊人的有用,帮我省下了无数浪费的时间。幸运的是,我已经按照servy的建议编写代码,但我使用的是Task构造函数。 - Adam Houldsworth
1
@Stephen Cleary:我认为你分享的有关在asp.net框架中使用async的见解是不可或缺的。我无法赞美你够多。继续保持好工作,伙计! - XDS
@Stephen Cleary 如果您编写了这样的单元测试,并且您没有“全程异步”,即该单元测试不是异步的,并且您在DownloadAndStoreAsync()上调用了.Wait(),那么死锁是否可能发生?意思是当运行单元测试时,它是否在类似UI上下文的上下文中运行,这可能会导致死锁,还是与控制台应用程序具有相同的上下文。 - Vlad
1
@Vlad:取决于你的单元测试框架。MSTest与控制台应用程序具有相同的上下文,除非你使用的是MSTest-for-UniversalWindows,它提供了UI-ish上下文。xUnit始终提供UI-ish上下文。NUnit不提供,除了一些辅助方法。所以有点混乱;检查SynchronizationContext.Current以确保正确性,或者直接使用比其他框架更可预测的xUnit。 - Stephen Cleary

8
您在使用await时遇到了经典的死锁问题,即您正在启动一个具有await的异步方法,然后在启动它后立即对该任务进行阻塞等待(当您在DownloadAndStore中调用Result时)。
当您调用await时,它会捕获SynchronizationContext.Current的值,并确保所有由await调用产生的延续都被发布回该同步上下文。
因此,您正在启动一个任务,它正在执行异步操作。为了继续进行其延续,它需要在某个时间点上释放同步上下文,以便可以处理该延续。
然后是来自调用者的代码(在同一同步上下文中),它正在等待任务。它不会放弃对该同步上下文的控制,直到任务完成,但任务需要同步上下文才能完成。现在,您有两个相互等待的任务;经典的死锁。
这里有几个选项。一个理想的解决方案是“一路异步”,并且从一开始就不要阻塞同步上下文。这很可能需要您的测试框架的支持。
另一个选择是仅确保您的await调用不会发布回同步上下文。您可以通过在所有await的任务中添加ConfigureAwait(false)来实现此目的。如果您这样做,您需要确保它是您真正程序中想要的行为,而不仅仅是您的测试框架。如果您的真实框架需要使用捕获同步上下文,则这不是一个选项。
您还可以创建自己的消息泵,具有自己的同步上下文,在每个测试范围内使用它。这允许测试本身阻塞,直到所有异步操作完成,但允许该消息泵内部的所有内容完全异步。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接