How to test a ViewModel that loads with a BackgroundWorker?

余生长醉 提交于 2019-11-30 08:51:53

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.

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