8

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...

6
  • Where is it when it's hanging? Also, is SynchronizationContext.Current null, or is there a context provided by your testing framework? Commented Oct 14, 2013 at 17:09
  • 1
    Side note, does DownloadAll really want to download all of the urls one at a time? Would you not rather parallelize them and have them all be running at the same time? Commented Oct 14, 2013 at 17:33
  • @Servy yes, that's pretty much what I'm trying to do... following this idea msdn.microsoft.com/en-us/library/vstudio/hh696703.aspx Commented Oct 14, 2013 at 19:29
  • So you want to download them one at a time? Because that's what you're doing. Commented Oct 14, 2013 at 19:30
  • thinking about it, that's probably not the best way to do it.... Commented Oct 14, 2013 at 19:34

2 Answers 2

20

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.

Sign up to request clarification or add additional context in comments.

5 Comments

@Alex Stephen Cleary knows.
+1 Without a single trace of sarcasm, that was shockingly useful and just saved me countless wasted time. Fortunately I was already coding it as servy's advice suggested, but I was using the Task constructor.
@Stephen Cleary: If find the insights you have shared on the use of async within the asp.net framework to be indispensable. I can't praise you enough. Keep up the good work mate!
@Stephen Cleary if you are writting a unit test like this, and you don't go "async all the way" i.e. the unit test is not async, and you call .Wait() on DownloadAndStoreAsync() , is a deadlock possible ? Meaning when the unit test is run, is it run in a context like the UI context, which could cause a deadlock, or it has the same context as a console app.
@Vlad: That depends on your unit test framework. MSTest has the same context as a console app, unless you're using the MSTest-for-UniversalWindows, which provides a UI-ish context. xUnit always provides a UI-ish context. NUnit doesn't, except for some of its helper methods which do. So it's a bit of a mess; check SynchronizationContext.Current to be sure, or just use xUnit which is more predictable than the others.
8

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.