6.7.2013

Part 2 - Testing: Reactive benefits with commodity option barrier events

In my last post I introduced some benefits of using reactive programming techniques. This post will start us down the journey of testing so that subsequent topics can easily be expressed and verified.

Extracting the Anti Corruption Layer

As previously mentioned, when working with APIs it's helpful to introduce an anti-corruption layer. This layer allows you to build strong separation between an external system and your own. Think of this layer as a condom, practice safe development! This layer is a great place to transform and filter interactions to constrain how you use the external system and to inject a layer that can be very helpful for testing.

FuturesQuoteClient publishes FuturesQuotes via an event. First, we'll build a small anti-corruption layer with transformed events. Then we can do the same with observables. The anti-corruption layer will transform a FuturesQuote to QuoteWithContract and exclude quotes with invalid contracts.

Anti-corruption event layer

Stubbing the real client

The FuturesQuoteClient for reference:

public class FuturesQuoteClient
{
    public delegate void QuoteHandler(object sender, FuturesQuote quote);

    public event QuoteHandler Quotes;

    ...
}

Most APIs that use events won't have the ability to stub them for testing. Also, in c#, only the defining class of an event can raise it. Therefore, to separate the real client we'll need a wrapper around it. This is like a wrapper before our anti-corruption wrapper :)

/// <summary>
///     An interface to abstract the quote source.
/// </summary>
public interface IFuturesQuoteClient
{
    event FuturesQuoteClient.QuoteHandler Quotes;
}

This allows us to abstract an interface which we can stub during testing. Here's an implementation of the wrapper for the real client:

/// <summary>
///     Implementation of real wrapper around quote source.
/// </summary>
public class FuturesQuoteClientWrapper : IFuturesQuoteClient
{
    public FuturesQuoteClientWrapper(FuturesQuoteClient client)
    {
        client.Quotes += (sender, quote) =>
            {
                var handler = Quotes;
                if (handler != null) handler(this, quote);
            };
    }

    public event FuturesQuoteClient.QuoteHandler Quotes;
}

The wrapper takes a FuturesQuoteClient and forwards quotes to the IFuturesQuoteClient.Quotes event interface. I'm not going to test this wrapper, I only included it to show the extra dimension of complexity required. This wrapper would require an integration test with the real quote service.

Testing the anti-corruption event layer

Now that we can isolate the anti-corruption layer from the real client, we can begin to build the functionality through tests.

First, we want to transform FuturesQuote to QuoteWithContract:

[Test]
public void OnFuturesQuote_AValidContractOnAFuturesQuote_TriggersQuoteWithContract()
{
    var validFuturesQuote = new FuturesQuote
        {
            Symbol = "CZ2013"
        };
    var futuresQuoteClient = MockRepository.GenerateStub<IFuturesQuoteClient>();
    var quotesWithContractClient = new AntiCorruptionLayerEventClient(futuresQuoteClient);
    var quotes = new List<NotifyOnBarrierEventsReactive.QuoteWithContract>();
    quotesWithContractClient.Quotes += (sender, quote) => quotes.Add(quote);

    futuresQuoteClient.Raise(c => c.Quotes += null, null, validFuturesQuote);

    quotes.Should().HaveCount(1);
    var quoteWithContract = quotes.Single();
    quoteWithContract.Quote.ShouldBeEquivalentTo(validFuturesQuote);
}

  • Setup
    • We start with a valid FuturesQuote.
    • I'm using Rhino.Mocks to generate a stub of IFuturesQuoteClient instead of creating this class manually.
    • AntiCorruptionLayerEventClient is the class that will implement the anti-corruption layer, it doesn't exist yet. It requires a client of type IFuturesQuoteClient.
    • To detect what events are raised, we subscribe to the transformed event AntiCorruptionLayerEventClient.Quotes and put the quotes into a list.
  • Act
    • Next, we raise the event on our IFuturesQuoteClient stub with the validFuturesQuote event argument.
  • Assert
    • Finally, we want to make sure only one QuoteWithContract arrives for the original validFuturesQuote.

Implementing AntiCorruptionLayerEventClient

First we need our transformed event:

public class AntiCorruptionLayerEventClient
{
    public delegate void QuoteWithContractHandler(object sender, NotifyOnBarrierEventsReactive.QuoteWithContract args);

    public event QuoteWithContractHandler Quotes;

    protected virtual void OnQuotes(NotifyOnBarrierEventsReactive.QuoteWithContract quote)
    {
        var handler = Quotes;
        if (handler != null) handler(this, quote);
    }

    ...
}

That's a lot of typing, events aren't fun!

To get the test to pass we have to transform and forward the quotes:

public class AntiCorruptionLayerEventClient
{
    ...

    public AntiCorruptionLayerEventClient(IFuturesQuoteClient client)
    {
        client.Quotes += TransformToQuoteWithContract;
        // neglects handling unsubscribing from the event and cascading that up the chain :)
    }

    private void TransformToQuoteWithContract(object sender, FuturesQuote quote)
    {
        OnQuotes(new NotifyOnBarrierEventsReactive.QuoteWithContract(quote));
    }
}

The constructor takes IFuturesQuoteClient, subscribes to FuturesQuote events and transforms them to QuoteWithContract. Finally, it raises the transformed event.

Pain points

  • Awkward to assert what events are raised.
  • Extra layer of indirection in IFuturesQuoteClient abstraction.
  • Excessive code to setup new events in the anti-corruption layer.
  • These code samples would also need unsubscribe and disposal taken into consideration.
  • Obsession with how (imperative) versus what (declarative).

Side Note: Unit testing transforms

The above example demonstrates testing the composition of the anti-corruption layer. Testing the actual transform is better left to a unit test where the infrastructure of the anti-corruption layer doesn't get in the way of more detailed testing. Here's one possible test:

[Test]
public void MapFromFuturesQuote()
{
    var quote = new FuturesQuote
        {
            Symbol = "CZ2013"
        };

    var commodityContract = NotifyOnBarrierEventsReactive.QuoteWithContract.ContractFromQuote(quote);

    commodityContract.ContractMonth.Should().Be(12);
    commodityContract.ContractYear.Should().Be(2013);
    commodityContract.ProductCode.Should().Be("C");
}

Notice how simple this test is! Regardless if we use events or observables these tests will look the same.

Anti-corruption observable alternative

Stubbing the real client

Now let's look at the same scenario with an anti-corruption layer built on observables. To isolate the client we still have to apply a wrapper, fortunately the reactive extensions provides this for us (as shown in the last post):

private static IObservable<FuturesQuote> FuturesQuotesSource(FuturesQuoteClient client)
{
    return Observable
        .FromEvent<FuturesQuoteClient.QuoteHandler, FuturesQuote>(h => client.Quotes += h, h => client.Quotes -= h);
}

Again, I'm not testing the wrapper to isolate our anti-corruption layer, that would be done in an integration test. However, since we're leveraging a built in wrapper, we don't have to worry about as many test cases as we would with our implementation of FuturesQuoteClientWrapper.

Testing the anti-corruption observable layer

Here's the equivalent test to verify quotes are transformed:

[Test]
public void OnFuturesQuote_WithAValidContract_StreamsQuoteWithContract()
{
    var validFuturesQuote = new FuturesQuote
        {
            Symbol = "CZ2013"
        };
    var scheduler = new TestScheduler();
    var futuresQuotes = scheduler.CreateColdObservable(ReactiveTest.OnNext(0, validFuturesQuote));
    var quotesWithContractClient = new AntiCorruptionLayerObservableClient(futuresQuotes);

    var quotesWithContracts = scheduler.Start(() => quotesWithContractClient.Quotes);

    quotesWithContracts.Messages.Should().HaveCount(1);
    var quoteWithContract = quotesWithContracts.Messages.Single().Value.Value;
    quoteWithContract.Quote.ShouldBeEquivalentTo(validFuturesQuote);
}

  • Setup
    • Again we start with a valid FuturesQuote.
    • The reactive extensions comes with testing abstractions in Rx-Testing. One of these is a TestScheduler
      • Schedulers dispatch work in the reactive framework
      • TestScheduler is optimized to control and monitor the testing process.
    • Our new client AntiCorruptionLayerObservableClient requires an IObservable<FuturesQuote>
      • TestScheduler.CreateColdObservable creates this observable for testing, there's more to learn about this API but for now those details aren't relevant.
      • We setup the validFuturesQuote message in the test observable.
  • Act
    • To run the test we tell the scheduler to Start which causes it to dispatch our validFuturesQuote message
    • The Start method takes an IObservable<T> and returns an ITestableObserver<T> where T is our transformed QuoteWithContract
      • The testable observer, note observer not observable, records message type, timing and values.
      • Time is irrelevant for Select transforms but will be invaluable later on.
  • Assert
    • Finally we assert the same condition as before, that only one quote was transformed for the provided validFuturesQuote

Pain points

  • The TestScheduler is designed to help with testing situations where time is involved. There's a bit of overhead to test Select operations where timing is irrelevant
    • Have to specify time (0 above) when setting up the observable scenario (scheduler.CreateColdObservable(ReactiveTest.OnNext(0, validFuturesQuote)))
    • Have to dig into messages skipping timing and message type to get to the value you want to verify (Messages.Single().Value.Value)

Benefits

  • Testing abstractions are built in
  • No awkward hacks to capture messages, it's part of the framework.
  • Focus on what (declarative), not how (imperative)

Implementing AntiCorruptionLayerObservableClient

There's one constructor to create the AntiCorruptionLayerObservableClient from the FuturesQuoteClient, and one to create this from IObservable<FuturesQuote>. These are equivalent to FuturesQuoteClientWrapper and AntiCorruptionLayerEventClient.ctor(IFuturesQuoteClient) respectively:

public class AntiCorruptionLayerObservableClient
{
    ...

    public AntiCorruptionLayerObservableClient(FuturesQuoteClient client)
        : this(FuturesQuotesSource(client))
    {
    }


    public AntiCorruptionLayerObservableClient(IObservable<FuturesQuote> quotes)
    {
        Quotes = quotes
            .Select(q => new NotifyOnBarrierEventsReactive.QuoteWithContract(q));
    }

    public IObservable<NotifyOnBarrierEventsReactive.QuoteWithContract> Quotes { get; private set; }
}

The only real code involved is in transforming the source observable, one call to Select!

Anti-corruption layer filtering

In the anti-corruption layer event sample I didn't go into excluding quotes with invalid contracts. It's trivial and I think the reactive sample is much more expressive:

[Test]
public void OnFuturesQuote_WithAnInvalidContract_StreamsNothing()
{
    var invalidFuturesQuote = new FuturesQuote
        {
            Symbol = "invalidcontract"
        };
    var scheduler = new TestScheduler();
    var futuresQuotes = scheduler.CreateColdObservable(ReactiveTest.OnNext(0, invalidFuturesQuote));
    var quotesWithContractClient = new AntiCorruptionLayerObservableClient(futuresQuotes);

    var quotesWithContracts = scheduler.Start(() => quotesWithContractClient.Quotes);

    quotesWithContracts.Messages.Should().BeEmpty();
}

To get this test to pass we simply add a filter:

public AntiCorruptionLayerObservableClient(IObservable<FuturesQuote> quotes)
{
    Quotes = quotes
        .Select(q => new NotifyOnBarrierEventsReactive.QuoteWithContract(q))
        .Where(NotifyOnBarrierEventsReactive.IsValidContract);
}

Conclusion

Implementing an anti-corruption layer around information streams with reactive programming offers several benefits over imperative approaches. I'll admit the benefits aren't as exciting without the time dependent aspects. However, when working with time it will become patently obvious that there's no real comparison.





comments powered by Disqus