3

I take the code from ReactiveUi website documentation and try to unit test it but it fails.
It is about invoking a command to cancel another one.

Below is the class to test.

public class SomeViewModel : ReactiveObject
{
    public SomeViewModel(IScheduler scheduler)
    {
        this.CancelableCommand = ReactiveCommand
            .CreateFromObservable(
                () => Observable
                    .StartAsync(DoSomethingAsync)
                    .TakeUntil(CancelCommand), outputScheduler: scheduler);
        this.CancelCommand = ReactiveCommand.Create(
            () =>
            {
                Debug.WriteLine("Cancelling");
            },
            this.CancelableCommand.IsExecuting, scheduler);
    }

    public ReactiveCommand<Unit, Unit> CancelableCommand
    {
        get;
        private set;
    }

    public ReactiveCommand<Unit, Unit> CancelCommand
    {
        get;
        private set;
    }

    public bool IsCancelled { get; private set; }

    private async Task DoSomethingAsync(CancellationToken ct)
    {
        try
        {
            await Task.Delay(TimeSpan.FromSeconds(3), ct);
        }
        catch (TaskCanceledException)
        {
            IsCancelled = true;
        }
    }
}

And here is the unit test:

[TestFixture]
[Category("ViewModels")]
public class SomeViewModelFixture
{
    public async Task Executing_cancel_should_cancel_cancelableTask()
    {
        var sut = new SomeViewModel(Scheduler.Immediate);
        var t = sut.CancelableCommand.Execute();
        await sut.CancelCommand.Execute();

        await t;

        Assert.IsTrue(sut.IsCancelled);
    }
}

The CancelCommand is excuted (a breakpoint on the Console log is hit) however the Task.Delay is never cancelled. It seems TakeUntil doesn't request the cancellation.

UPDATE
I edited the code above so that my ViewModel ctor takes a IScheduler and create the commands with it as per that issue about unit testing but the test still fails.
I tried both nUnit and xUnit.
I also tried to do RxApp.MainThreadScheduler = Scheduler.Immediate in my test setup as per that article but still fails.

UPDATE2
From the solution that I marked as the answer and the comments, the simplest is not to use IScheduler in the ctor and then to write the test like that and it passes.

[Test]
    public async Task Executing_cancel_should_cancel_cancelableTask()
    {
        var sut = new SomeViewModel();
        sut.CancelableCommand.Execute().Subscribe();
        await sut.CancelCommand.Execute();

        Assert.IsTrue(sut.IsCancelled);
    }
François
  • 3,164
  • 25
  • 58
  • Perhaps, you need to take a look at using the unit test runner that's not deferred. A quick search found this article which may help. http://putridparrot.com/blog/unit-testing-a-reactiveui-viewmodel/ – kenny Feb 17 '17 at 17:36
  • Tks but: i) I don't fully understand ii) RxApp doesnt have aDeferredScheduler property ii) in the article the author says reactiveui was detecting test runners and was doing the scheduler switch. – François Feb 17 '17 at 18:07
  • DeferredScheduler was renamed to MainThreadScheduler. I made it the Immediate scheduler but test still fails. I'm using NUnit. Will try with xUnit. – François Feb 17 '17 at 18:08
  • Issue remains with Immediate Scheduler and on both nUnit and xUnit. – François Feb 17 '17 at 18:15
  • @François if it is a test/learning project, would you mind to share so I could try to debug and see what's happening? – Giusepe Feb 17 '17 at 19:18
  • I'm learning ReactiveUI and that code is exactly what I intend to use so unit test have to work first. – François Feb 17 '17 at 19:20
  • Wait, isn't you missing a Subscribe there? command.Execute().Subscribe(); – Giusepe Feb 17 '17 at 19:21
  • I tried already... Anyway both commands do execute but the canceleable is just not cancelled – François Feb 17 '17 at 19:23

2 Answers2

3

This is working for me:

public class SomeViewModelTest
{
    SomeViewModel m_actual;

    [SetUp]
    public void Setup()
    {
        m_actual = new SomeViewModel(CurrentThreadScheduler.Instance); 
        m_actual.Activator.Activate();
    }

    [Test]
    public void Executing_cancel_should_cancel_cancelableTask()
    {
        m_actual.CancelableCommand.Execute().Subscribe();
        m_actual.CancelCommand.Execute().Subscribe();

        Assert.IsTrue(m_actual.IsCancelled);
    } 
}

I changed the scheduler to use the same from the test itself and my ViewModel does implement the ISupportsActivation, that I dare to say won't do any diference here. Other then that, I removed the async/await from the test, you don't need that with Rx, and just subscribed to the command.

Giusepe
  • 655
  • 5
  • 15
  • 1
    Indeed the Subscribe make it works (not the setup nor the Activator). I don't understand why though. I'll look deeper into the doc. Tks anyway :-) – François Feb 17 '17 at 19:48
  • The scheduler is useless as well. The solution only comes from the subscribe. – François Feb 17 '17 at 20:03
  • 1
    So a thought on that one... When you call Execute it calls the function that you passed into ReactiveCommand. So in your case that Function is the Observable.StartAsync which retrieves the task but at this point the observable itself still hasn't been subscribed to even though the Task was retrieved. So in your original code where you await the Cancel the "CancelableCommand" still hasn't technically been started. So the CancelCommand runs and then finishes running and then after that you "await t" but now the CancelCommand has already ran. Hopefully that makes sense – Shane Neuville Feb 17 '17 at 20:03
  • Like in your original code if you subscribe to t directly after execute. Then you comment out "await t" then the unit test passes – Shane Neuville Feb 17 '17 at 20:04
  • @ShaneNeuville It starts to make sense tks. I guess ReactiveUI has a steep learning curve then it'll become natural. – François Feb 17 '17 at 20:05
  • 1
    Yea but it's worth it :-) And I just want to point out that this is kind of more about understanding how Observable.StartAsync works than how RxUI works. When you call Observable.StartAsync that's going to execute your Task method immediately but the Observable itself technically still hasn't started. So if you want to delay execution of the Task then you need to surround the Observable.StartAsync with an Observable.Defer – Shane Neuville Feb 17 '17 at 20:07
  • @François one thing I feel like will also help with learning Rx is to just abandon the tasks or put something in front of them. Adding that interaction complexity into the mix makes it all get kind of confusing kind of fast. If you have libraries that only expose Tasks then maybe just make a wrapper around those libraries and expose the tasks with something like Observable.FromAsync – Shane Neuville Feb 17 '17 at 20:35
  • 1
    That will be the hard part. I'm new not only to ReactiveUI but to Rx programming. I'm very used to tasks not so much to observables. One step at a time – François Feb 17 '17 at 20:39
2

The problem is that you're awaiting the execution of the command after you cancel it. Since the cancellation occurs before the command executes, it doesn't affect the command's execution. You can get it to pass as follows:

public async Task Executing_cancel_should_cancel_cancelableTask()
{
    var sut = new SomeViewModel(Scheduler.Immediate);
    sut.CancelableCommand.Execute().Subscribe();
    await sut.CancelCommand.Execute();

    Assert.True(sut.IsCancelled);
}

Here I'm starting the execution of the command immediately (by subscribing). The subsequent cancellation then affects that execution.

As a general note, it's always a bit messy mixing Rx and TPL. It works, but there are pitfalls and nasties like this lurking around every corner. As a longer-term solution, I'd highly recommend moving to "pure" Rx. You won't look back - it's amazing.

Kent Boogaart
  • 175,602
  • 35
  • 392
  • 393
  • Await a task after cancelling it works with tasks. I'll take your advice and avoid mixing tasks and observables . – François Feb 18 '17 at 08:15