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.
"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" - I don't follow that explaination, they are both observable sequences which never pump.
ReplyDeleteA simpler explanation is; Generate is called once, the thing that it returns is subscribed to 5 times.
Generate is called multiple times and you need to make sure what's returned is non faulted stream - the first attempt doesn't do that
ReplyDeleteDunno how well this will format but:
ReplyDelete[SetUp]
public void SetUp()
{
_testScheduler = new TestScheduler();
var called = 0;
_retryCount = 0;
_barService = new Mock();
_barService.Setup(x => x.Generate()).Returns(
() => Observable.Create(o =>
{
Debug.WriteLine("Retry {0}", ++_retryCount);
return Observable.Never().Subscribe(o);
})).Callback(() =>
{
++called;
if(called > 1)
Console.WriteLine("oh");
});
}
The point is, the first attempt at testing the retry does not use Observable.Create for the moq setup...
ReplyDeleteHey Ollie,
ReplyDeleteJust wanted to clear this up, sorry i wasn't clearer from the start.
Yeah, you are right you need the Create to verify the Retry operator is resubscribing to the stream - its important to note that the method itself is only called once though. For this reason I find that it is better to expose IObservabe streams as properties (unless they are paramterised). In this case maybe
IObservabe Bars { get; }
The faulted stream thing is also interesting - Observabe.Empty is a do-nothing cold-observable, it can carry no state so cannot be 'faulted'. Indeed as far as I am aware it is the responsibility of the Observer to ignore OnNext's after an OnError so you could say that only a subscription could be errored. Using Observabe.Create and the lambda-based subscribe extensions methods shield you from behaving badly on both sides.
HTHs
Cheers
"only a subscription could be errored" sorry for the obviuous bullshit - whilst well behaved observers do ignore OnNexts after OnErrors, well behaved IObservables do not call OnNext after an OnError - so indeed the stream is faulted.
ReplyDelete