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

时间:2013-10-14 17:07:59

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

    _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设置中的某些内容应该归咎于......

2 个答案:

答案 0 :(得分:16)

你的问题出现在这个模拟中:

new Task<IEnumerable<MobileContent>>(() => new List<MobileContent>())

您不应在异步代码中使用Task构造函数。相反,请使用Task.FromResult

Task.FromResult<IEnumerable<MobileContent>>(new List<MobileContent>())

我建议您阅读我的MSDN articleasync 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的所有任务中,从而实现此目的。如果您这样做,您需要确保它在您的真实程序中的行为,而不仅仅是您的测试框架。如果您的真实框架需要使用捕获同步上下文,那么这不是一个选项。

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