How to Unit test ViewModel with async initialization in WPF

别等时光非礼了梦想. 提交于 2020-02-25 02:53:09

问题


I have created a sample WPF MVVM project which I now want to Unit test. The viewmodels load the data asynchronously in the constructor:

public class CustomerOverviewViewModel
{
   public CustomerOverviewViewModel()
   {
       var t = LoadFullCustomerListAsync();
   }
   public async Task LoadFullCustomerListAsync()
   {
      List<BL_acc> customers = await Task.Run(() => // Query from db);
   }
}

In WPF, this works like a charm. When I want to create a unit test for this viewmodel, I create the object by calling its default constructor:

  [TestMethod]
  public void Test()
  {
      customerOverviewViewModel = new CustomerOverviewViewModel();
      // Do the test
  }

However, the unit test has no way of knowing when the async method is finished. Can this be fixed using constructor initialization or should I use a different pattern?

Edit

The unit tests don't need the async loaded information, they just need an instance of the class to test the methods. It just seems like a lot of extra work to use another initialization method just for my unit tests.

All the unit tests succeed but they sometimes throw an error that multiple threads try to access the same context (which disappears when the data is not loaded async):

The context cannot be used while the model is being created. This exception may be thrown if the context is used inside the OnModelCreating method or if the same context instance is accessed by multiple threads concurrently. Note that instance members of DbContext and related classes are not guaranteed to be thread safe.


回答1:


In WPF, this works like a charm.

Sort of. As currently written, the "initialization" task is just ignored. So, there's no error handling, and there's no indication to the UI that the initialization is in progress or completed (i.e., there's no way for it to know when to show a spinner).

In other words, surfacing this information (state as well as the resulting data) would be useful to more than just the unit test code. I have a straightforward data-bindable task wrapper that I wrote a while ago to help in situations like this.

There isn't an alternative way to detect the completion of asynchronous methods if they return a Task; you'd have to expose the Task somehow. I think the best way of exposing it is something like the type I wrote, so that the UI can make use of it as well.




回答2:


Stephen's answer has all the relevant information. I'd still finish mine to show how you'd actually unit-test an asynchronous ViewModel.

In a nutshell, your model might look like this:

public class CustomerOverviewViewModel : INotifyPropertyChanged 
{
    readonly Task<string> _dataTask;
    readonly Task _notifierTask;

    public event PropertyChangedEventHandler PropertyChanged = delegate {};

    public string Data
    {
        get
        {
            return _dataTask.IsCompleted ? 
                _dataTask.GetAwaiter().GetResult() : 
                "loading...";
        }
    }

    public CustomerOverviewViewModel()
    {
        _dataTask = LoadFullCustomerListAsync();

        Func<Task> notifier = async () =>
        {
            try 
            {           
                await _dataTask;
            }
            finally
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs("Data"));
            }
        };

        // any exception thrown by _dataTask stays dormant in _notifierTask
        // and may go unobserved until GC'ed, but it will be re-thrown 
        // to the whenever CustomerOverviewViewModel.Data is accessed
        // from PropertyChanged event handlers
        _notifierTask = notifier(); 
    }

    async Task<string> LoadFullCustomerListAsync()
    {
        await Task.Delay(1000);
        return "42";
    }
}

Your unit test would now be asynchronous, too:

[TestMethod]
public async Task Test()
{
    var customerOverviewViewModel = new CustomerOverviewViewModel();

    var tcs = new TaskCompletionSource<bool>();

    PropertyChangedEventHandler handler = (s, e) =>
    {
        if (e.PropertyName == "Data")
            tcs.TrySetResult(true);
    };

    customerOverviewViewModel.PropertyChanged += handler;
    try 
    {
        await tcs.Task;
    }
    finally
    {
        customerOverviewViewModel.PropertyChanged -= handler;
    }

    Assert.IsTrue(customerOverviewViewModel.Data == "42");
}


来源:https://stackoverflow.com/questions/31289216/how-to-unit-test-viewmodel-with-async-initialization-in-wpf

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!