Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I unit test an async ICommand in MVVM?

I've been Googling and even Bing-ing and I haven't come up with anything that is satisfying.

I have a ViewModel which has some commands, such as: SaveCommand, NewCommand and DeleteCommand. My SaveCommand executes a save to a file operation, which I want to be an async operation so that the UI doesn't wait for it.

My SaveCommand is an instance of AsyncCommand, which implements ICommand.

 SaveCommand = new AsyncCommand(
  async param =>
        {
            Connection con = await Connection.GetInstanceAsync(m_configurationPath);
            con.Shoppe.Configurations = new List<CouchDbConfig>(m_configurations);
            await con.SaveConfigurationAsync(m_configurationPath);
            //now that its saved, we reload the Data.
            await LoadDataAsync(m_configurationPath);
        }, 
 ...etc

Now I'm building a test for my ViewModel. In it, I create a new thing with the NewCommand, I modify it and then use the SaveCommand.

vm.SaveCommand.Execute(null);
Assert.IsFalse(vm.SaveCommand.CanExecute(null));

My CanExecute method (not shown) of the SaveCommand should return False just after the item has been saved (there's no point saving an unchanged item). However, the Assert shown above fails all the time because I am not waiting for the SaveCommand to finish executing.

Now, I can't wait for it to finish executing because I can't. The ICommand.Execute doesn't return a Task. And if I change the AsyncCommand to have its Execute return a Task then it won't implement the ICommand interface properly.

So, the only thing I think I can do now, for testing purposes, is for the AsynCommand to have a new function:

public async Task ExecuteAsync(object param) { ... }

And thus, my test will run (and await) the ExecuteAsync function and the XAML UI will run the ICommand.Execute method in which it does not await.

I don't feel happy about doing my proposed solution method as I think, and hope, and wish that there is a better way.

Is what I suggest, reasonable? Is there a better way?

like image 541
Peter pete Avatar asked Apr 28 '15 12:04

Peter pete


1 Answers

Review of the other answers

Doing while (vm.SaveCommand.Executing) ; seems like busy waiting and I prefer to avoid that.

The other solution using AsyncCommand from Stephen Cleary seems a bit overkill for such a simple task.

My proposed way doesn't break encapsulation - the Save method doesn't expose any internals. It just offers another way of accessing the same functionality.

My solution seems to cover everything that's needed in a simple and straightforward way.

Suggestion

I would suggest refactoring this code:

SaveCommand = new AsyncCommand(
    async param =>
    {
        Connection con = await Connection.GetInstanceAsync(m_configurationPath);
        con.Shoppe.Configurations = new List<CouchDbConfig>(m_configurations);
        await con.SaveConfigurationAsync(m_configurationPath);
        //now that its saved, we reload the Data.
        await LoadDataAsync(m_configurationPath);
    });

to:

SaveCommand = new RelayCommand(async param => await Save(param));

public async Task Save(object param)
{
    Connection con = await Connection.GetInstanceAsync(m_configurationPath);
    con.Shoppe.Configurations = new List<CouchDbConfig>(m_configurations);
    await con.SaveConfigurationAsync(m_configurationPath);
    //now that its saved, we reload the Data.
    await LoadDataAsync(m_configurationPath);
}

Just a note: I changed AsyncCommand to RelayCommand which can be found in any MVVM framework. It just receives an action as parameter and runs that when the ICommand.Execute method is called.

The unit tests

I made an example using the NUnit framework which has support for async tests:

[Test]
public async Task MyViewModelWithAsyncCommandsTest()
{
    // Arrange
    // do view model initialization here

    // Act
    await vm.Save(param);

    // Assert
    // verify that what what you expected actually happened
}

and in the view bind the command like you would do normally:

Command="{Binding SaveCommand}"
like image 131
Igor Popov Avatar answered Sep 20 '22 18:09

Igor Popov