Подтвердить что ты не робот

Как проверить ViewModel, который загружается с BackgroundWorker?

Одна из приятных вещей о MVVM - это возможность проверки ViewModel. В моем конкретном случае у меня есть виртуальная машина, которая загружает некоторые данные при вызове команды и соответствующий ей тест:

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

Однако это кажется действительно неуклюжим... Это работает, но просто чувствует себя грязным. Любые лучшие предложения?


Возможное решение:

На основании ответа Дэвида Холла, чтобы издеваться над 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 для производства. Затем My 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().

4b9b3361

Ответ 1

Ввести макет/фальшивый фоновый работник, который проверяет, что вы его правильно назовете, но сразу возвращается с сохраненным ответом.

Измените свою модель представления, чтобы разрешить инъекцию зависимостей, либо путем инъекции свойств, либо инсталляции конструктора (я показываю инъекцию конструктора ниже), а затем при тестировании вы проходите в поддельном фоновом работнике. В реальном мире вы вводите реальную реализацию при создании виртуальной машины.

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 в вашем случае) проводка правильного фона работника не должна быть слишком сложной задачей.

Единственная проблема с этим подходом заключается в том, что большинство классов Microsoft, включая BackGroundWorker, не реализуют интерфейсы, поэтому фальсификация/издевка над ними может быть сложной.

Лучший подход, который я нашел, - это создать свой собственный интерфейс для объекта для издевки, а затем объект-оболочку, который находится поверх реального класса Microsoft. Не идеально, поскольку у вас есть тонкий слой непроверенного кода, но, по крайней мере, это означает, что непроверенная поверхность вашего приложения переходит в рамки тестирования и от кода приложения.

Ответ 2

Вы можете избежать дополнительной абстракции, если вы хотите обменять ее на небольшое количество загрязнений модели (например, ввод кода, который используется только для ваших тестов) следующим образом:

Сначала добавьте дополнительный конструктор AutoResetEvent (или ManualResetEvent) к вашему конструктору модели просмотра и убедитесь, что вы "установили" этот экземпляр AutoResetEvent, когда ваш фоновый рабочий закончит обработчик "RunWorkerCompleted".

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

Теперь вы можете передать экземпляр как часть вашего 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
}

Это именно то, для чего были разработаны классы AutoResetEvent (и ManualResetEvent). Поэтому, несмотря на небольшое загрязнение модельного кода модели, я думаю, что это решение довольно аккуратно.