【问题标题】:How to test a ViewModel that loads with a BackgroundWorker?如何测试使用 BackgroundWorker 加载的 ViewModel?
【发布时间】:2011-06-23 13:46:59
【问题描述】:

MVVM 的优点之一是 ViewModel 的可测试性。在我的特殊情况下,我有一个在调用命令时加载一些数据的 VM,以及它的相应测试:

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);
}

所以这都是非常简单的东西。但是,我真正想做的是使用BackgroundWorker 加载数据,并在屏幕上显示“加载”消息。所以我把虚拟机改成:

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();
}

这在运行时视觉效果很好,但是我的测试现在失败了,因为没有立即加载属性。任何人都可以提出一个测试这种加载的好方法吗?我想我需要的是这样的:

[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.");
}

然而,这看起来真的很笨重......它有效,但感觉很脏。 有更好的建议吗?


最终解决方案:

根据 David Hall 的回答,为了模拟 BackgroundWorker,我最终围绕 BackgroundWorker 做了一个相当简单的包装器,它定义了两个类,一个异步加载数据,一个同步加载。

  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));
      }
    }
  }

那么在我的 Unity 配置中,我可以使用 SyncWorker 进行测试,使用 AsyncWorker 进行生产。然后我的 ViewModel 变成:

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

请注意,我标记为 wcfClient 的东西在我的测试中实际上也是一个 Mock,所以在调用 vm.LoadDataCommand.Execute() 之后,我还可以验证是否调用了 wcfClient.LoadData()。

【问题讨论】:

  • 在 Caliburn 中添加 CoROutine 支持也会有所帮助。
  • "定义了两个类,一个异步加载数据,一个同步加载。"哇,那个模式听起来like something I've seen before...
  • @Will - 你提到这一点很有趣。在发布我的编辑后,我发现部分 Prism 指南谈到使用 SynchronizationContext 但它的编写方式对我来说只有 1/2 有意义,并且没有真正提到测试。我必须找到一个使用它的好例子,看看是否可以代替它。
  • 实际上,Prism 指南还提到了使用 WPF 的 Dispatcher,这里也提到了:stackoverflow.com/questions/1949789/…,但这似乎不太可测试。
  • 您的最终解决方案对我帮助很大。谢谢。

标签: c# wpf unit-testing mvvm backgroundworker


【解决方案1】:

引入一个模拟/假后台工作程序,它会验证您是否正确调用它,但会立即返回一个预设响应。

更改您的视图模型以允许注入依赖项,通过属性注入或构造函数注入(我在下面显示构造函数注入),然后在测试时传入假后台工作程序。在现实世界中,您在创建 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();    
    }

}

根据您的框架(在您的情况下为 Unity/Prism),连接正确的后台工作人员应该不会太难。

这种方法的一个问题是,包括 BackGroundWorker 在内的大多数 Microsoft 类都没有实现接口,因此伪造/模拟它们可能会很棘手。

我发现的最佳方法是为要模拟的对象创建自己的接口,然后创建一个位于实际 Microsoft 类之上的包装器对象。不理想,因为您有一层薄薄的未经测试的代码,但至少这意味着您的应用程序未经测试的表面会移入测试框架并远离应用程序代码。

【讨论】:

  • 这是个好主意。我们正在使用 Prism 和 Unity,所以我可以连接 Unity 以注入模拟 BgWorker 进行测试。有趣的建议。
  • @rally25rs 对于某些标准值,这是执行此操作的“标准”方式 :) 我正在对这个概念进行更多讨论。
  • 这里是接口/继承后台workerstackoverflow.com/questions/848179/…
  • +1 - 这回答了一个我没有意识到我迫切需要回答的问题。
  • “这种方法的一个问题是,包括 BackGroundWorker 在内的大多数 Microsoft 类都没有实现接口,因此伪造/模拟它们可能很棘手。” - 是的,我认为这就是我一开始没有想到这一点的原因。我没有看到模拟它的好方法。如果我必须包装整个事情,我可能只是制作某种BGWorkerFactory 来注入,而不是单个 BGWorker,因为 VM 可能一次加载超过 1 个数据元素。如果我想出一个工作工厂/包装器,我会将它附加到我的问题中。
【解决方案2】:

如果您愿意用它来换取少量的视图模型污染(即引入仅用于测试的代码),则可以避免额外的抽象,如下所示:

首先向您的视图模型构造函数添加一个可选的 AutoResetEvent(或 ManualResetEvent),并确保在后台工作人员完成“RunWorkerCompleted”处理程序时“设置”此 AutoResetEvent 实例。

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();
  }
}

现在您可以在单元测试中传递一个实例。

[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
}

这正是 AutoResetEvent(和 ManualResetEvent)类的设计目的。所以除了轻微的视图模型代码污染之外,我认为这个解决方案是相当整洁的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多