MVVM + TPL + Unit tests
UPD: This implementation didn’t work as well as expected, please read the follow-up article.
Recently I’ve faced an issue with writing unit tests for view model commands that use Task Parallel Library for asynchronous operations. Searching over the web was not really helpful, so I spent some time designing the approach to handle such cases.
The most common way to start tasks with TPL is by using one of the static Task.Factory.StartNew(…) methods. If you are mockist TDD guy, this should already raise an alert in your head. Static members are not test-friendly and do not allow you to inject mock dependencies into your module. One may say that using tasks is just an implementation details and should not be concerned when testing the behavior. I would agree if we were speaking about a regular async method that allows you to perform some action when its execution completes (in a form of callback or event, doesn’t really matter).
That simply doesn’t work for me because the asynchrony is used inside the command handler, so there is no way to specify the continuation outside of the view model.
My first (and probably the bad one) idea was to create a service for running the tasks. Then it would be easy to mock it and run tasks synchronously. Hopefully, I rejected this solution – creating another dependency just didn’t sound right.
I decided to create a special ICommand implementation instead that would notify me when the execution completes. This should do the trick because you can always use the thread sync primitives in the test environment to wait on the asynchronous task.
For simplicity’s sake I just created the wrapper around RelayCommand from MVVM Light Toolkit (you could use your favorite library or implement the command from scratch yourself).
public class AsyncDelegateCommand: ICommand
{
private readonly RelayCommand _innerCommand;
public event EventHandler ExecuteCompleted;
public event EventHandler CanExecuteChanged
{
add { _innerCommand.CanExecuteChanged += value; }
remove { _innerCommand.CanExecuteChanged -= value; }
}
public AsyncDelegateCommand(
Action<AsyncDelegateCommand> executeMethod,
Func<bool> canExecuteMethod)
{
_innerCommand = new RelayCommand(
() => executeMethod(this),
canExecuteMethod);
}
public void Execute(object parameter)
{
_innerCommand.Execute(null);
}
public bool CanExecute(object parameter)
{
return _innerCommand.CanExecute(null);
}
public void NotifyCompleted()
{
var handler = ExecuteCompleted;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
Sample application
The test harness application is fairly simple. The single button starts a ‘long-running’ asynchronous task. The button is disabled while the task is running.
public class MainViewModel : ViewModelBase
{
private bool _isRunning;
public bool IsRunning
{
get { return _isRunning; }
set
{
if (_isRunning != value)
{
_isRunning = value;
RaisePropertyChanged("IsRunning");
}
}
}
public AsyncDelegateCommand StartLongOperationCommand
{
get;
private set;
}
public MainViewModel()
{
StartLongOperationCommand =
new AsyncDelegateCommand(OnStartLongOperation,
() => true);
}
private void OnStartLongOperation(AsyncDelegateCommand command)
{
IsRunning = true;
Task.Factory
.StartNew(() => Thread.Sleep(5000))
.ContinueWith(task =>
{
// update UI
IsRunning = false;
command.NotifyCompleted();
}, TaskScheduler.FromCurrentSynchronizationContext());
}
}
The command handler starts the main task and specifies its continuation that updates the UI properties and notifies when the command is completed. Note that I used the UI task scheduler for the continuation. In this particular case it doesn’t make much sense because WPF automatically dispatches INotifyPropertyChanged events on the UI thread, but our ‘update UI’ logic could be more sophisticated (i.e. ObservableCollection modifications).
Unit Tests
[TestClass]
public class MainViewModelTests
{
[TestMethod]
public void CommandShouldNotifyAboutCompletion()
{
var context = new SynchronizationContext();
SynchronizationContext.SetSynchronizationContext(context);
bool notified = false;
var sync = new ManualResetEvent(false);
var target = GetTarget();
target.StartLongOperationCommand.ExecuteCompleted += (s, e) =>
{
notified = true;
sync.Set();
};
target.StartLongOperationCommand.Execute(null);
sync.WaitOne();
Assert.IsTrue(notified);
}
private MainViewModel GetTarget()
{
return new MainViewModel();
}
}
Points of interest:
- Using ManualResetEvent as a thread synchronization primitive. The main test thread awaits until the sync is set to signaled state on the delegate handler completion.
- Setting current SynchronizationContext value. UI update continuation task is scheduled on the TaskScheduler, acquired from the current synchronization context. When TaskScheduler.FromCurrentSynchronizationContext is called in the test environment, it throws an exception by default (TaskScheduler.Current and TaskScheduler.Default are both set to null). So we explicitly create and set a new task scheduler.
Download sources (TaskCommands.zip)