The RelayCommand is an excellent way to communicate between a view and View Model in MVVM without creating a link to a specific instance of either. However; one problem that I recently encountered when using these was: how are they unit tested.
Take the following piece of code (taken from https://tfsutilities.codeplex.com/):
public class MainViewModel : ViewModelBase
{
public RelayCommand FindOrphanedWorkspaceCommand { get ; private set ; }
This is instantiated here:
public MainViewModel( IDataService dataService)
{
...
FindOrphanedWorkspaceCommand = new RelayCommand(FindOrphanedWorkspaces);
And, finally, the method itself:
/// /// Call method to retrieve all workspaces that currently have no pending changes
///
private void FindOrphanedWorkspaces()
{
// Doesn't really matter what this actually does
So; can we just write a test; what about this:
[TestClass]
public class MainViewModelTest
{
[ TestMethod]
public void FindOrphanedWorkspaces()
{
IDataService ds = new Mock.MockDataService ();
MockMainViewModel mvm = new MockMainViewModel (ds);
mvm.FindOrphanedWorkspaceCommand();
}
The answer, of course, is no (otherwise this wouldn’t be a particularly useful blog post).
The Workaround
There is a workaround (which is pretty much by fallback workaround these days when I find something can’t be unit tested for reasons of protection):
Simply change the private method to protected and subclass the viewmodel:
class MockMainViewModel : MainViewModel
{
public MockMainViewModel( IDataService dataService) : base (dataService) { }
public void FindOrphanedWorkspaces()
{
base.FindOrphanedWorkspaces();
This certainly works and, in its defence, it tests as completely as calling the RelayCommand (I don’t believe testing the MVVM architecture is within the remit of the test architecture of any dependent program).
However…
It feels like a lot of additional work (as it happens, it’s work you may have to do anyway for other things, but that’s beside the point). So, what’s the alternative?
RelayCommand implements ICommand, here’s the metadata:
namespace GalaSoft.MvvmLight.Command
{
// Summary:
// A command whose sole purpose is to relay its functionality to other objects
// by invoking delegates. The default return value for the CanExecute method
// is 'true'. This class does not allow you to accept command parameters in
// the Execute and CanExecute callback methods.
public class RelayCommand : ICommand
{
// Summary:
// Initializes a new instance of the RelayCommand class that can always execute.
//
// Parameters:
// execute:
// The execution logic.
//
// Exceptions:
// System.ArgumentNullException:
// If the execute argument is null.
public RelayCommand( Action execute);
//
// Summary:
// Initializes a new instance of the RelayCommand class.
//
// Parameters:
// execute:
// The execution logic.
//
// canExecute:
// The execution status logic.
//
// Exceptions:
// System.ArgumentNullException:
// If the execute argument is null.
public RelayCommand( Action execute, Func< bool> canExecute);
// Summary:
// Occurs when changes occur that affect whether the command should execute.
public event EventHandler CanExecuteChanged;
// Summary:
// Defines the method that determines whether the command can execute in its
// current state.
//
// Parameters:
// parameter:
// This parameter will always be ignored.
//
// Returns:
// true if this command can be executed; otherwise, false.
public bool CanExecute( object parameter);
//
// Summary:
// Defines the method to be called when the command is invoked.
//
// Parameters:
// parameter:
// This parameter will always be ignored.
public virtual void Execute( object parameter);
//
// Summary:
// Raises the GalaSoft.MvvmLight.Command.RelayCommand.CanExecuteChanged event.
public void RaiseCanExecuteChanged();
}
}
Consequently, you can simply do this:
[ TestMethod]
public void FindOrphanedWorkspaces()
{
IDataService ds = new Mock.MockDataService ();
MockMainViewModel mvm = new MockMainViewModel (ds);
mvm.FindOrphanedWorkspaceCommand.Execute( null);
}
And, there’s more. If you’ve implemented the CanExecute, you can test if that works; for example:
Assert.IsTrue(mvm.FindOrphanedWorkspaceCommand.CanExecute(null));
Or
Assert.IsFalse(mvm.FindOrphanedWorkspaceCommand.CanExecute(null));
In my case, as it stands, the former.
Conclusion
So, we can check whether the command can execute, and whether it does execute. Admittedly this isn’t ground-breaking research, but it took me longer that it should have to figure it out, and next time, I’ll have a blog post to refer to.