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...
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With