Problem
You have an observable that is dependent on time, and want to write a unit test that is not dependent on time. Observables that depend on time include ones that use timeouts, windowing/buffering, and throttling/sampling. You want to unit test these but do not want your unit tests to have excessive runtimes.
Solution
It’s certainly possible to put delays in your unit tests; however, there are two problems with that approach: 1) the unit tests take a long time to run, and 2) there are race conditions because the unit tests all run at the same time, making timing unpredictable.
The Rx library was designed with testing in mind; in fact, the Rx library itself is exten‐
sively unit tested. To enable this, Rx introduced a concept called a scheduler, and every Rx operator that deals with time is implemented using this abstract scheduler.
To make your observables testable, you need to allow your caller to specify the scheduler.
For example, we can take the MyTimeoutClass from Recipe 6.5 and add a scheduler:
public interface IHttpService {
76 | Chapter 6: Testing
IObservable<string> GetString(string url);
}
public class MyTimeoutClass {
private readonly IHttpService _httpService;
public MyTimeoutClass(IHttpService httpService) {
_httpService = httpService;
}
public IObservable<string> GetStringWithTimeout(string url, IScheduler scheduler = null)
{
return _httpService.GetString(url)
.Timeout(TimeSpan.FromSeconds(1), scheduler ?? Scheduler.Default);
} }
Next, let’s modify our HTTP service stub so that it also understands scheduling, and we’ll introduce a variable delay:
private class SuccessHttpServiceStub : IHttpService {
public IScheduler Scheduler { get; set; } public TimeSpan Delay { get; set; }
public IObservable<string> GetString(string url) {
return Observable.Return("stub") .Delay(Delay, Scheduler);
} }
Now you can use TestScheduler, a type included in the Rx library. TestScheduler gives you powerful control over (virtual) time.
TestScheduler is in a separate NuGet package from the rest of Rx;
you’ll need to install the Rx-Testing NuGet package.
TestScheduler gives you complete control over time, but you often just need to set up your code and then call TestScheduler.Start. Start will virtually advance time until everything is done. A simple success test case could look like this:
[TestMethod]
public void MyTimeoutClass_SuccessfulGetShortDelay_ReturnsResult() {
6.6. Unit Testing Rx Observables with Faked Scheduling | 77
var scheduler = new TestScheduler();
var stub = new SuccessHttpServiceStub {
Scheduler = scheduler,
Delay = TimeSpan.FromSeconds(0.5), };
var my = new MyTimeoutClass(stub);
string result = null;
my.GetStringWithTimeout("http://www.example.com/", scheduler) .Subscribe(r => { result = r; });
scheduler.Start();
Assert.AreEqual("stub", result);
}
The code simulates a network delay of half a second. It’s important to note that this unit test does not take half a second to run; on my machine, it takes about 70 milliseconds.
The half-second delay only exists in virtual time. The other notable difference in this unit test is that it is not asynchronous; since we are using TestScheduler, all our tests can complete immediately.
Now that everything is using test schedulers, it’s easy to test timeout situations:
[TestMethod]
public void MyTimeoutClass_SuccessfulGetLongDelay_ThrowsTimeoutException() {
var scheduler = new TestScheduler();
var stub = new SuccessHttpServiceStub {
Scheduler = scheduler,
Delay = TimeSpan.FromSeconds(1.5), };
var my = new MyTimeoutClass(stub);
Exception result = null;
my.GetStringWithTimeout("http://www.example.com/", scheduler)
.Subscribe(_ => Assert.Fail("Received value"), ex => { result = ex; });
scheduler.Start();
Assert.IsInstanceOfType(result, typeof(TimeoutException));
}
Once again, this unit test does not take 1 second (or 1.5 seconds) to run; it executes immediately using virtual time.
78 | Chapter 6: Testing
Discussion
We’ve just scratched the surface on Reactive Extensions schedulers and virtual time. I recommend that you start unit testing when you start writing Rx code; as your code grows more complex, rest assured that the Rx testing is capable of handling it.
TestScheduler also has AdvanceTo and AdvanceBy methods, which allow you to grad‐
ually step through virtual time. There may be situations where these are useful, but you should strive to have your unit tests only test one thing. For example, when testing a timeout, you could write a single unit test that advanced the TestScheduler partially and ensured the timeout did not happen early and then advanced the TestScheduler past the timeout value and ensured the timeout did happen. However, I prefer to have independent unit tests as much as possible, for example, one unit test ensuring that the timeout did not happen early, and a different unit test ensuring that the timeout did happen later.
See Also
Recipe 6.5 covers the basics of unit testing observable sequences.
6.6. Unit Testing Rx Observables with Faked Scheduling | 79