5

I'm not sure the correct way to structure this test. I've got a view model here:

public class ViewModel
{
  public ReactiveCommand PerformSearchCommand { get; private set; }
  private readonly ObservableAsPropertyHelper<bool> _IsBusy;
  public bool IsBusy
  {
      get { return _IsBusy.Value; }
  }

  public ViewModel(IAdventureWorksRepository _awRepository)
  {
    PerformSearchCommand = new ReactiveCommand();

    PerformSearchCommand.RegisterAsyncFunction((x) =>
    {
      return _awRepository.vIndividualCustomers.Take(1000).ToList();
    }).Subscribe(rval =>
    {
      CustomerList = rval;
      SelectedCustomer = CustomerList.FirstOrDefault();
    });

    PerformSearchCommand.IsExecuting.ToProperty(this, x => x.IsBusy, out _IsBusy);
    PerformSearchCommand.Execute(null); // begin executing immediately
  }

}

The dependency is a data access object to AdventureWorks

public interface IAdventureWorksRepository
{
  IQueryable<vIndividualCustomer> vIndividualCustomers { get; }
}

Finally, my test looks something like this:

[TestMethod]
public void TestTiming()
{
    new TestScheduler().With(sched =>
    {
        var repoMock = new Mock<IAdventureWorksRepository>();

        repoMock.Setup(x => x.vIndividualCustomers).Returns(() =>
        {
            return new vIndividualCustomer[] {
            new vIndividualCustomer { FirstName = "John", LastName = "Doe" }
        };
        });

        var vm = new ViewModel(repoMock.Object);

        Assert.AreEqual(true, vm.IsBusy); //fails?
        Assert.AreEqual(1, vm.CustomerList.Count); //also fails, so it's not like the whole thing ran already

        sched.AdvanceTo(2);
        Assert.AreEqual(1, vm.CustomerList.Count); // success
        // now the customer list is set at tick 2 (not at 1?)  
        // IsBusy was NEVER true.

    });



    
}

So the viewmodel should immediately begin searching upon load

My immediate problem is that the IsBusy property doesn't seem to get set in the testing scheduler, even though it seems to work fine when I run the code normally. Am I using the ToProperty method correctly in the view model?

More generally, what is the proper way to do the full 'time travel' testing when my object under test has a dependency like this? The issue is that unlike most testing examples I'm seeing, the called interface is not an IObservable. It's just a synchronous query, used asynchronously in my view model. Of course in the view model test, I can mock the query to do whatever rx things I want. How would I set this up if I wanted the query to last 200 ticks, for example?

JiBéDoublevé
  • 4,124
  • 4
  • 36
  • 57
Clyde
  • 8,017
  • 11
  • 56
  • 87

1 Answers1

12

So, you've got a few things in your code that is stopping you from getting things to work correctly:

Don't invoke commands in ViewModel Constructors

First, calling Execute in the constructor means you'll never see the state change. The best pattern is to write that command but not execute it in the VM immediately, then in the View:

this.WhenAnyValue(x => x.ViewModel)
    .InvokeCommand(this, x => x.ViewModel.PerformSearchCommand);

Move the clock after async actions

Ok, now that we can properly test the before and after state, we have to realize that after every time we do something that normally would be async, we have to advance the scheduler if we use TestScheduler. This means, that when we invoke the command, we should immediately advance the clock:

Assert.IsTrue(vm.PerformSearchCommand.CanExecute(null));

vm.PerformSearchCommand.Execute(null);
sched.AdvanceByMs(10);

Can't test Time Travel without IObservable

However, the trick is, your mock executes code immediately, there's no delay, so you'll never see it be busy. It just returns a canned value. Unfortunately, injecting the Repository makes this difficult to test if you want to see IsBusy toggle.

So, let's rig the constructor a little bit:

public ViewModel(IAdventureWorksRepository _awRepository, Func<IObservable<List<Customer>>> searchCommand = null)
{
    PerformSearchCommand = new ReactiveCommand();

    searchCommand = searchCommand ?? () => Observable.Start(() => {
        return _awRepository.vIndividualCustomers.Take(1000).ToList();
    }, RxApp.TaskPoolScheduler);

    PerformSearchCommand.RegisterAsync(searchCommand)
        .Subscribe(rval => {
            CustomerList = rval;
            SelectedCustomer = CustomerList.FirstOrDefault();
        });

    PerformSearchCommand.IsExecuting
        .ToProperty(this, x => x.IsBusy, out _IsBusy);
}

Set up the test now

Now, we can set up the test, to replace PerformSearchCommand's action with something that has a delay on it:

new TestScheduler().With(sched =>
{
    var repoMock = new Mock<IAdventureWorksRepository>();

    var vm = new ViewModel(repoMock.Object, () => 
        Observable.Return(new[] { new vIndividualCustomer(), })
            .Delay(TimeSpan.FromSeconds(1.0), sched));

    Assert.AreEqual(false, vm.IsBusy);
    Assert.AreEqual(0, vm.CustomerList.Count);

    vm.PerformSearchCommand.Execute(null);
    sched.AdvanceByMs(10);

    // We should be busy, we haven't finished yet - no customers
    Assert.AreEqual(true, vm.IsBusy);
    Assert.AreEqual(0, vm.CustomerList.Count);

    // Skip ahead to after we've returned the customer
    sched.AdvanceByMs(1000);

    Assert.AreEqual(false, vm.IsBusy);
    Assert.AreEqual(1, vm.CustomerList.Count);
});
JiBéDoublevé
  • 4,124
  • 4
  • 36
  • 57
Ana Betts
  • 73,868
  • 16
  • 141
  • 209
  • Thanks for the detailed response. I guess the primary question I have right now is immediate execution of an async command. In my mind, I want the sort of object that will assert IsBusy == true after instantiation. What in particular is wrong with executing a command in the constructor. Is this a "Not Currently Implemented" behavior, or a "Couldn't Possibly Ever Work" sort of behavior? – Clyde Feb 06 '14 at 14:41
  • Executing a command in the ctor certainly *works*, it will just make your life difficult. Now every time you want to test the VM you have to stop search from doing anything real. This gets doubly worse when VMs interact with each other – Ana Betts Feb 06 '14 at 16:34
  • OK, but it didn't actually seem to work (in test). Are you saying that if I replaced my data repository with the observable source I would have been able to assert 'true == IsBusy' at time 0? – Clyde Feb 06 '14 at 17:25
  • Lol, dont try it, i just spent all this time telling you it was a bad idea – Ana Betts Feb 06 '14 at 17:57
  • :-) sorry, I didn't totally buy that -- feels like the tool is forcing me into a worse design rather than a better one in that case. I'm in an experimentation/evaluation mode with rx right now, not writing production code. – Clyde Feb 06 '14 at 18:17
  • Not sure what you mean, 'have to stop search from doing anything real'. In a test that dependency is mocked or stubbed anyway, so not doing anything is the default state. And the sort of screen I'm envisioning here is one where all but the most trivial of tests will require data to be loaded anyway. – Clyde Feb 06 '14 at 18:19