cmeeren cmeeren - 1 month ago 6
C# Question

Is it possible to unit test exceptions in Commands wrapping an async lambda?

Consider the following highly simplified viewmodel for fetching and showing a list of projects:

public class ProjectListViewModel
{
private readonly IWebService _webService;
public ICommand RefreshCommand { get; }
// INotifyPropertyChanged implementation skipped for brevity
public ObservableCollection<Project> Projects { get; set; }

public ProjectListViewModel(IWebService serverApi)
{
_serverApi = serverApi;
// ICommand implemented by Xamarin.Forms
RefreshCommand = new Command(async () => await RefreshAsync());
}

private async Task RefreshAsync()
{
try
{
Projects = await _webService.GetProjectsAsync();
}
catch (TaskCanceledException)
{
// Empty (task only cancelled when we are navigating away from page)
}
}
}


Using NUnit and Moq, I'm trying test that when
GetProjectsAsync
throws a
TaskCanceledException
, the ViewModel will catch it. The closest I get is this:

[Test]
public void When_Refreshing_Catches_TaskCanceledException()
{
// Arrange
webService = new Mock<IServerApi>();
webService.Setup(mock => mock.GetProjectsAsync())
.ThrowsAsync(new TaskCanceledException());
vm = new ProjectListViewModel(webService.Object);

// Act and assert
Assert.That(() => vm.RefreshCommand.Execute(null), Throws.Nothing);
}


The test passes, but unfortunately it's faulty - it still passes if I throw e.g. Exception instead of TaskCanceledException. As far as I know, the reason is that the exception doesn't bubble up past the command lambda,
async () => await RefreshAsync()
, so no exception thrown by GetProjectsAsync will ever be detected by the test. (When running the actual app however, the TaskCanceledException will bubble up and crash the app if not caught. I suspect this is related to synchronization contexts, of which I have very limited understanding.)

It works if I debug the test - if I mock it to throw Exception, it will break on the line with the command/lambda definition, and if I throw TaskCanceledException, the test will pass.

Note that the results are the same if I use Throws instead of ThrowsAsync. And in case it's relevant, I'm using the test runner in ReSharper 2016.2.

Using nUnit, is it possible at all to unit test exceptions thrown when executing "async" commands like this? Is it possible without writing a custom Command implementation?

Answer

Your problem is here:

new Command(async () => await RefreshAsync())

This async lambda is converted to an async void method by the compiler.

In my article on async best practices, I explain why the exception cannot be caught like this. async methods cannot propagate their exceptions directly (since their stack can be gone by the time the exception happens). async Task methods solve this naturally by placing the exception on their returned task. async void methods are unnatural, and they have nowhere to place the exception, so they raise it directly on the SynchronizationContext that was current at the time the method started.

In your application, this is the UI context, so it's just like it was thrown directly in an event handler. In your unit test, there is no context, so it's thrown on a thread pool thread. I think NUnit's behavior in this situation is to catch the exception and dump it to the console.

Personally, I prefer using my own asynchronous-compatible ICommand such as AsyncCommand in my Mvvm.Async library (also see my article on asynchronous MVVM commands):

new AsyncCommand(_ => RefreshAsync())

which can then be naturally unit tested:

await vm.RefreshCommand.ExecuteAsync(null); // does not throw

Alternatively, you can provide your own synchronization context in the unit test (using, e.g., my AsyncContext):

// Arrange
webService = new Mock<IServerApi>();
webService.Setup(mock => mock.GetProjectsAsync())
    .ThrowsAsync(new TaskCanceledException());
vm = new ProjectListViewModel(webService.Object);

// Act/Assert
AsyncContext.Run(() => vm.RefreshCommand.Execute(null));

In this case, if there was an exception, Run would propagate it.