How to test a ViewModel that loads with a BackgroundWorker?

Viewed 2819

One of the nice things about MVVM is the testability of the ViewModel. In my particular case, I have a VM that loads some data when a command is called, and its corresponding test:

public class MyViewModel
{
    public DelegateCommand LoadDataCommand { get; set; }

    private List<Data> myData;
    public List<Data> MyData
    {
        get { return myData; }
        set { myData = value; RaisePropertyChanged(() => MyData); }
    }

    public MyViewModel()
    {
        LoadDataCommand = new DelegateCommand(OnLoadData);
    }

    private void OnLoadData()
    {
        // loads data over wcf or db or whatever. doesn't matter from where...
        MyData = wcfClient.LoadData();
    }
}

[TestMethod]
public void LoadDataTest()
{
    var vm = new MyViewModel();
    vm.LoadDataCommand.Execute();
    Assert.IsNotNull(vm.MyData);
}

So that is all pretty simple stuff. However, what I would really like to do is load the data using a BackgroundWorker, and have a 'loading' message displayed on screen. So I change the VM to:

private void OnLoadData()
{
    IsBusy = true; // view is bound to IsBusy to show 'loading' message.

    var bg = new BackgroundWorker();
    bg.DoWork += (sender, e) =>
    {
      MyData = wcfClient.LoadData();
    };
    bg.RunWorkerCompleted += (sender, e) =>
    {
      IsBusy = false;
    };
    bg.RunWorkerAsync();
}

This works fine visually at runtime, however my test now fails because of the property not being loaded immediately. Can anyone suggest a good way to test this kind of loading? I suppose what I need is something like:

[TestMethod]
public void LoadDataTest()
{
    var vm = new MyViewModel();
    vm.LoadDataCommand.Execute();

    // wait a while and see if the data gets loaded.
    for(int i = 0; i < 10; i++)
    {
        Thread.Sleep(100);
        if(vm.MyData != null)
            return; // success
    }
    Assert.Fail("Data not loaded in a reasonable time.");
}

However that seems really clunky... It works, but just feels dirty. Any better suggestions?


Eventual Solution:

Based on David Hall's answer, to mock a BackgroundWorker, I ended up doing this fairly simple wrapper around BackgroundWorker that defines two classes, one that loads data asynchronously, and one that loads synchronously.

  public interface IWorker
  {
    void Run(DoWorkEventHandler doWork);
    void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete);
  }

  public class AsyncWorker : IWorker
  {
    public void Run(DoWorkEventHandler doWork)
    {
      Run(doWork, null);
    }

    public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
    {
      var bg = new BackgroundWorker();
      bg.DoWork += doWork;
      if(onComplete != null)
        bg.RunWorkerCompleted += onComplete;
      bg.RunWorkerAsync();
    }
  }

  public class SyncWorker : IWorker
  {
    public void Run(DoWorkEventHandler doWork)
    {
      Run(doWork, null);
    }

    public void Run(DoWorkEventHandler doWork, RunWorkerCompletedEventHandler onComplete)
    {
      Exception error = null;
      var args = new DoWorkEventArgs(null);
      try
      {
        doWork(this, args);
      }
      catch (Exception ex)
      {
        error = ex;
        throw;
      }
      finally
      {
        onComplete(this, new RunWorkerCompletedEventArgs(args.Result, error, args.Cancel));
      }
    }
  }

So then in my Unity configuration, I can use SyncWorker for testing, and AsyncWorker for production. My ViewModel then becomes:

public class MyViewModel(IWorker bgWorker)
{
    public void OnLoadData()
    {
        IsBusy = true;
        bgWorker.Run(
          (sender, e) =>
          {
            MyData = wcfClient.LoadData();
          },
          (sender, e) =>
          {
            IsBusy = false;
          });
    }
}

Note that the thing i have marked as wcfClient is actually a Mock in my tests too, so after the call to vm.LoadDataCommand.Execute() I can also validate that wcfClient.LoadData() was called.

2 Answers
Related