开发者

How to test a ViewModel that loads with a BackgroundWorker?

开发者 https://www.devze.com 2023-03-15 03:04 出处:网络
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:

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.


Introduce a mock/fake background worker which verifies that you call it correctly but returns immediately with a canned response.

Change your view model to allow for injection of dependencies, either through property injection or constructor injection (I'm showing constructor injection below) and then when testing you pass in the fake background worker. In the real world you inject the real implementation when creating the VM.

public class MyViewModel
{
    private IBackgroundWorker _bgworker;

    public MyViewModel(IBackgroundWorker bgworker)
    {
        _bgworker = bgworker;
    }

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

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

}

Depending on your framework (Unity/Prism in your case) wiring up the correct background worker should not be too hard to do.

The one problem with this approach is that most Microsoft classes including BackGroundWorker don't implement interfaces so faking/mocking them can be tricky.

The best approach I've found is to create your own interface for the object to mock and then a wrapper object that sits on top of the actual Microsoft class. Not ideal since you have a thin layer of untested code, but at least that means the untested surface of your app moves into testing frameworks and away from application code.


You can avoid the extra abstraction if you are willing to trade it for a small amount of view model pollution (i.e. introducing code that is only used for the sake of your tests) as follows:

First add an optional AutoResetEvent (or ManualResetEvent) to your view model constructor and make sure that you "set" this AutoResetEvent instance when your background worker finishes the "RunWorkerCompleted" handler.

public class MyViewModel {   
  private readonly BackgroundWorker _bgWorker;
  private readonly AutoResetEvent _bgWorkerWaitHandle;

  public MyViewModel(AutoResetEvent bgWorkerWaitHandle = null) {
    _bgWorkerWaitHandle = bgWorkerWaitHandle;

    _bgWorker = new BackgroundWorker();
    _bgWorker.DoWork += (sender, e) => {          
      //Do your work
    };        
    _bgworker.RunWorkerCompleted += (sender, e) => {          
      //Configure view model with results

      if (_bgWorkerWaitHandle != null) {
         _bgWorkerWaitHandle.Set();
      }
    };
    _bgWorker.RunWorkerAsync();
  }
}

Now you can pass in an instance as part of your unit test.

[Test]
public void Can_Create_View_Model() {
  var bgWorkerWaitHandle = new AutoResetEvent(false); //Make sure it starts off non-signaled
  var viewModel = new MyViewModel(bgWorkerWaitHandle);
  var didReceiveSignal = bgWorkerWaitHandle.WaitOne(TimeSpan.FromSeconds(5));
  Assert.IsTrue(didReceiveSignal, "The test timed out waiting for the background worker to complete.");
  //Any other test assertions
}

This is precisely what the AutoResetEvent (and ManualResetEvent) classes were designed for. So aside from the slight view model code pollution, I think this solution is quite neat.

0

精彩评论

暂无评论...
验证码 换一张
取 消