Testing Rx

In this Channel9 video, Wes and me explain the new testing functionality that now ships as part of Rx.

As the video goes into great detail on why you’d want to test Rx this way, I won’t go into details here. However as the video briefly touches on a lot of different subjects, here is a sample to get you started writing tests using this functionality.

First we’re going to create a base class that will provide some helpers to our test:

   public abstract class Test

    {

        public Recorded<Notification<T>> OnNext<T>(long ticks, T value)

        {

            return new Recorded<Notification<T>>(ticks, new Notification<T>.OnNext(value));

        }

        public Recorded<Notification<T>> OnCompleted<T>(long ticks)

        {

            return new Recorded<Notification<T>>(ticks, new Notification<T>.OnCompleted());

        }

        public Recorded<Notification<T>> OnError<T>(long ticks, Exception exception)

        {

            return new Recorded<Notification<T>>(ticks,
new Notification<T>.OnError(exception));

        }

        public Subscription Subscribe(long start, long end)

        {

            return new Subscription(start, end);

        }

        public Subscription Subscribe(long start)

        {

            return new Subscription(start);

        }

    }

Next we’re going to write a test to test the behavior of the Select operator. We’re using the Visual Studio Test Framework here, but it really can be done in any framework.

Here is the skeleton code of our test:

 

    [TestClass]

    public class SelectTest : Test

    {

        [TestMethod]

        public void Select_Complete()

        {

        }

    }

Let’s start filling in the blanks:

First we’ll create a test scheduler:

 

    var scheduler = new TestScheduler();

Next we need an observable that our Select operator can operate on:

 

    var xs = scheduler.CreateHotObservable(

                OnNext(180, 1),

                OnNext(210, 2),

                OnNext(240, 3),

                OnNext(290, 4),

                OnNext(350, 5),

                OnCompleted<int>(400),

                OnNext(410, -1),

                OnCompleted<int>(420),

                OnError<int>(430, new MockException(-1)));

Notice that this observable doesn’t follow the contract of Rx, as it produces more messages after the first OnCompleted. This is ok as we want to test that the Select operator still behaves correctly in this case.

Next we want to run the Select operator over this observable.

 

The test scheduler code has a set of details to make our live easier:

 

  • execute the code creating the observable at virtual time 100
  • subscribe to the resulting observable at virtual time 200
  • unsubscribe from the subscription at virtual time 1000

 

These are merely defaults, you’re free to change these around, but they have worked out well in most cases for our testing.

 

So with this information, we tell the test scheduler what to do to create the observable and run the test:

 

    var results = scheduler.Run(() => xs.Select(x =>

            {

                invoked++;

                return x + 1;

  }));

In this test, we side effect inside the select, to ensure that it gets invoked exactly as many times as we expect next to this we increment each value coming through by one, to ensure that the select operator is indeed doing the job it’s supposed to do.

Next we need to make sure that the resulting observable contained exactly the values we expected at exactly the right virtual time we expected.

The Run method helps us out here as it gives us back an IEnumerable<Recorded<Notification<int>>.

With the help of the AssertEqual extension method, we can verify the results.

    results.AssertEqual(

                OnNext(210, 3),

                OnNext(240, 4),

                OnNext(290, 5),

                OnNext(350, 6),

                OnCompleted<int>(400));

As you can see a lot fewer values than our input, this is due to the following reasons:

  • Any message sent out on the hot observable before subscription time (200) will be missed
  • Any message sent out after the first OnCompleted or OnError will be ignored

Another thing we’d like to check is that the Select operator is as strict as possible in its subscription to the underlying observable.

Meaning it subscribes only once, as soon as it is needed and unsubscribes as soon as it is no longer needed:

   xs.Subscriptions.AssertEqual(Subscribe(200, 400));

Finally we check if the Select operator has been called exactly as frequent as we’d expected:

 

    Assert.AreEqual(4, invoked);

Our final test code looks as follows:

 

    [TestClass]

    public class SelectTest : Test

    {

        [TestMethod]

        public void Select_Complete()

        {

            var scheduler = new TestScheduler();

            var invoked = 0;

            var xs = scheduler.CreateHotObservable(

                OnNext(180, 1),

                OnNext(210, 2),

                OnNext(240, 3),

                OnNext(290, 4),

                OnNext(350, 5),

                OnCompleted<int>(400),

                OnNext(410, -1),

                OnCompleted<int>(420),

                OnError<int>(430, new MockException(-1)));

            var results = scheduler.Run(() => xs.Select(x =>

            {

                invoked++;

                return x + 1;

            }));

            results.AssertEqual(

                OnNext(210, 3),

                OnNext(240, 4),

                OnNext(290, 5),

                OnNext(350, 6),

                OnCompleted<int>(400));

      xs.Subscriptions.AssertEqual(

                Subscribe(200, 400));

            Assert.AreEqual(4, invoked);

        }

    }

We have just implemented our first Rx test. It is the Hello World equivalent for Rx testing. This operator doesn’t involve a lot of complicated scheduling, but it shows all the required basics for using this kind of testing on more complicated scenarios.

 

Let us know what you think, if anything is missing or can be simplified. As always, feedback is most welcome at our forum.