Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing async method - test never completes

I'm trying to unit test my a class I'm building that calls a number of URLs (Async) and retrieves the contents.

Here's the test I'm having a problem with:

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

The relevant parts of DownloadContent are:

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

When my test runs, it never completes - it just hangs.

I suspect something in the setup of my _mockDownloadContent is to blame...

like image 778
Alex Avatar asked Oct 14 '13 17:10

Alex


2 Answers

Your problem is in this mock:

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

You should not use the Task constructor in asynchronous code. Instead, use Task.FromResult:

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

I recommend you read my MSDN article or async blog post which point out that the Task constructor should not be used for async code.

Also, I recommend you do take Servy's advice and do async "all the way" (this is also covered by my MSDN article). If you use await properly, your code would change to look like this:

public async Task DownloadAndStoreAsync()
{
    //service passed in through ctor
    var urls = _getContentUrls.GetAll();
    var content = await DownloadAllAsync(urls);
    //do stuff with content here
}

with your test looking like:

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

Note that modern versions of NUnit understand async Task unit tests without any problems.

like image 123
Stephen Cleary Avatar answered Oct 16 '22 04:10

Stephen Cleary


You're running into the classic deadlock issue when using await in which you're starting up an asynchronous method that has an await in it, and then after starting it you're immediately doing a blocking wait on that task (when you call Result in DownloadAndStore).

When you call await it will capture the value of SynchronizationContext.Current and ensure that all of the continuations resulting from an await call are posted back to that sync context.

So you're starting a task, and it's doing an async operation. In order for it to continue on to it's continuation it needs the sync context to be "free" at some point so that it can process that continuation.

Then there's code from the caller (in that same sync context) that is waiting on the task. It won't give up it's hold on that sync context until the task finishes, but the task needs the sync context to be free for it to finish. You now have two tasks waiting on each other; a classic deadlock.

There are several options here. One, the ideal solution, is to "async all the way up" and never block the sync context to begin with. This will most likely require support from your testing framework.

Another option is to just ensure that your await calls don't post back to the sync context. You can do this by adding ConfigureAwait(false) to all of the tasks that you await. If you do this you'll need to ensure that it's the behavior that you want in your real program as well, not just your testing framework. If your real framework requires the use of the captures sync contexts then that's not an option.

You could also create your own message pump, with it's own synchronization context, that you use within the scope of each test. This allows the test itself to block until all asynchronous operations are complete, but allows everything inside of that message pump to be entirely asynchronous.

like image 34
Servy Avatar answered Oct 16 '22 05:10

Servy