Earlier this week I was trying to unit test an asynchronous service (Foo) which used another asynchronous service (Bar) internally and ran into an issue trying to mock out the Bar service so that it would cause the retry & timeout schedules to fire.
Bar is defined as follows, the implementation is irrelevant as it being mocked for the tests:
Foo is similarly defined:
The implementation of the Foo service is the important part, it uses the Boo service to generate a value, it's expected to generate the value or Timeout, if it fails to generate a value (for what ever reason) it's expected to to Retry:
You can see I've set the time out to be 10 seconds and a maximum of 5 attempts...
So I wanted to put Timeout & Retry under test, to do this we use moq for mocking out our dependencies, we also use nCrunch for continuous testing, you'll see this in the screenshots below as the red\green icons on the left-hand side of the code - these icons tell me where the code is failing etc..
As you can see two identical tests, asserting on different behaviours of the FooService, you'll notice the constructor of the FooService is being injected with an instance of the BarService, this is being created as Mock<T> of the IBarService interface.
You'll also notice the use of the MS TestScheduler for Rx, essential for unit testing anything in Rx.
So my first attempt at mocking this out looked like this:
When the tests were run I didn't expected either of the tests to fail, but what I got was one successful & one failed test! The Timeout had worked but the Retry schedule hadn't!
My initial thought was I hadn't passed the scheduler to the Rx Retry method in the FooService implementation, but after checking I realised it doesn'tneed take the scheduler so the problem must be with the test setup. To be more precise the problem must have been with what was being returned for the mocked Generate method on the IBarService.
This was indeed the problem, the mock should have been returning a Func which created an observable sequence which never pumps instead of what I initially had, a Func returning a sequence which never pumped:
The change is subtle but makes a big difference when testing:
Looking at the decompiled code it tells me why, the Retry extension method is using the Catch extension method to replace the faulted source stream with another, it just so happens that happens to be the source stream, so therefore from a mocking point of view you need to make sure that every time the Return Func is executed it creates a valid non-faulted stream.
Bar is defined as follows, the implementation is irrelevant as it being mocked for the tests:
1: public interface IBarService
2: {
3: IObservable<Unit> Generate();
4: }
Foo is similarly defined:
1: public interface IFooService
2: {
3: IObservable<Unit> Generate();
4: }
The implementation of the Foo service is the important part, it uses the Boo service to generate a value, it's expected to generate the value or Timeout, if it fails to generate a value (for what ever reason) it's expected to to Retry:
1: public class FooService : IFooService
2: {
3: private readonly IBarService _barService;
4: private readonly IScheduler _scheduler;
5:
6: public FooService(IBarService barService, IScheduler scheduler)
7: {
8: _barService = barService;
9: _scheduler = scheduler;
10: }
11:
12: public IObservable<Unit> Generate()
13: {
14: return _barService.Generate()
15: .Timeout(TimeSpan.FromSeconds(10), _scheduler)
16: .Retry(5)
17: .Select(bar => Unit.Default);
18: }
19: }
You can see I've set the time out to be 10 seconds and a maximum of 5 attempts...
So I wanted to put Timeout & Retry under test, to do this we use moq for mocking out our dependencies, we also use nCrunch for continuous testing, you'll see this in the screenshots below as the red\green icons on the left-hand side of the code - these icons tell me where the code is failing etc..
1: [Test]
2: public void should_timeout()
3: {
4: // ARRANGE
5: Exception exception = null;
6: var fooService = new FooService(_barService.Object, _testScheduler);
7:
8: // ACT
9: fooService.Generate()
10: .ObserveOn(_testScheduler)
11: .Subscribe(_ => { }, exn => exception = exn);
12:
13: _testScheduler.AdvanceBy(TimeSpan.FromHours(1).Ticks);
14:
15: // ASSERT
16: Assert.That(exception, Is.Not.Null);
17: }
18:
19: [Test]
20: public void should_retry()
21: {
22: // ARRANGE
23: Exception exception = null;
24: var fooService = new FooService(_barService.Object, _testScheduler);
25:
26: // ACT
27: fooService.Generate()
28: .ObserveOn(_testScheduler)
29: .Subscribe(_ => { }, exn => exception = exn);
30:
31: _testScheduler.AdvanceBy(TimeSpan.FromHours(1).Ticks);
32:
33: // ASSERT
34: Assert.That(_retryCount, Is.EqualTo(5));
35: }
As you can see two identical tests, asserting on different behaviours of the FooService, you'll notice the constructor of the FooService is being injected with an instance of the BarService, this is being created as Mock<T> of the IBarService interface.
You'll also notice the use of the MS TestScheduler for Rx, essential for unit testing anything in Rx.
So my first attempt at mocking this out looked like this:
1: [SetUp]
2: public void SetUp()
3: {
4: _testScheduler = new TestScheduler();
5:
6: _retryCount = 0;
7: _barService = new Mock<IBarService>();
8: _barService.Setup(x => x.Generate()).Returns(
9: () =>
10: {
11: Debug.WriteLine("Retry {0}", ++_retryCount);
12: return Observable.Never<Unit>();
13: });
14: }
When the tests were run I didn't expected either of the tests to fail, but what I got was one successful & one failed test! The Timeout had worked but the Retry schedule hadn't!
My initial thought was I hadn't passed the scheduler to the Rx Retry method in the FooService implementation, but after checking I realised it doesn't
This was indeed the problem, the mock should have been returning a Func which created an observable sequence which never pumps instead of what I initially had, a Func returning a sequence which never pumped:
1: [SetUp]
2: public void SetUp()
3: {
4: _testScheduler = new TestScheduler();
5:
6: _retryCount = 0;
7: _barService = new Mock<IBarService>();
8: _barService.Setup(x => x.Generate()).Returns(
9: () =>
10: {
11: return Observable.Create<Unit>(o =>
12: {
13: Debug.WriteLine("Retry {0}", ++_retryCount);
14: return Observable.Never<Unit>().Subscribe(o);
15: });
16: });
17: }
The change is subtle but makes a big difference when testing:
Looking at the decompiled code it tells me why, the Retry extension method is using the Catch extension method to replace the faulted source stream with another, it just so happens that happens to be the source stream, so therefore from a mocking point of view you need to make sure that every time the Return Func is executed it creates a valid non-faulted stream.