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"));
}

相关部分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是罪魁祸首......

4

2 回答 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毫无问题地理解单元测试。

于 2013-10-14T17:30:13.427 回答
8

您在使用时遇到了经典的死锁问题,await在该问题中您启动了一个包含一个异步方法,await然后在启动它之后,您立即对该任务执行阻塞等待(当您调用Resultin时DownloadAndStore)。

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

因此,您正在启动一项任务,并且它正在执行异步操作。为了让它继续它的延续,它需要同步上下文在某个时候是“免费的”,以便它可以处理该延续。

然后有来自调用者的代码(在同一个同步上下文中)正在等待任务。在任务完成之前,它不会放弃对同步上下文的持有,但任务需要同步上下文空闲才能完成。您现在有两个任务在等待对方;经典的僵局。

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

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

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

于 2013-10-14T17:17:27.410 回答