繁体   English   中英

单元测试异步方法 - 测试永远不会完成

[英]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的异步方法时,你遇到了经典的死锁问题,然后在启动之后你就会立即对该任务进行阻塞等待(当你调用ResultDownloadAndStore )。

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

所以你正在开始一项任务,它正在进行异步操作。 为了使它继续延续,它需要同步上下文在某个时刻“自由”,以便它可以处理该延续。

然后是来自调用者的代码(在同一个同步上下文中)等待任务。 在任务完成之前,它不会放弃它对同步上下文的保持,但是任务需要同步上下文可以自由完成。 你现在有两个任务在彼此等待; 经典的僵局。

这里有几种选择。 其中一个理想的解决方案是“一直异步”并且永远不会阻止同步上下文。 这很可能需要您的测试框架的支持。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM