Friday, July 9, 2010

Unit testing, how do I unit test a WCF Service with dependency OperationContext?

Unit testing WCF Services is a lot like testing Web Services, especially in that they represent remote business logic. However, in some cases testing may not be straightforward, such as when referencing static framework class OperationContext,

// a simple subscription based service.
[ServiceContract (CallbackContract = typeof (ISubscriberCallback))]
public interface ISubscriptionService
{
    // push a subject to service
    [OperationContract]
    void Publish (string subject);

    // subscribe client to publications.
    [OperationContract]
    void Subscribe ();

    // unsubscribe client from publications.
    [OperationContract]
    void Unsubscribe ();
}

// a simple client-side subscription callback.
public interface ISubscriberCallback
{
    // push a subject to subscribers
    [OperationContract]
    void Receive (string subject);
}

// a possible implementation of subscription service
[ServiceBehavior (
    ConcurrencyMode = ConcurrencyMode.Multiple, 
    InstanceContextMode = InstanceContextMode.Single)]
public class SubscriptionService : ISubscriptionService
{

    private readonly object _syncRoot = new object ();
    private readonly List<ISubscriberCallback> _subscribers = 
        new List<ISubscriberCallback> ();

    public void Publish (string subject) { ... }

    public void Subscribe ()
    {
        // hm, static framework references ... 
        ISubscriberCallback subscriber = 
            OperationContext.
            Current.
            GetCallbackChannel<ISubscriberCallback> ();

        lock (_syncRoot)
        {
            if (!_subscribers.Contains (subscriber))
            {
                _subscribers.Add (subscriber);
            }
        }
    }

    public void Unsubscribe () { ... }

}

Unfortunately, if our service implements some sophisticated business logic while initializing callbacks then we risk either losing coverage with no tests or losing time and resources on elaborate environments to support our integration into WCF framework.

Well not to fear, this is strikingly familiar to our Web Service with Context dependency, and I would proscribe a similar solution. Namely,

// a simple abstraction to separate our business from 
// WCF framework. depending on our requirements, could be
// general for re-use elsewhere in our application code,
// or specific if this is a one-off use.
public interface IOperationContext
{
    // add methods as needed, for now we definitely
    // need access to our callbacks!
    CType GetCallbackChannel<CType> ();
}

// a simple wrapper implementation of operation context
public class OperationContextCurrent : IOperationContext
{
    public CType GetCallbackChannel<CType> ()
    {
        return OperationContext.
            Current.
            GetCallbackChannel<CType> ();
    }
}

[ServiceBehavior (
    ConcurrencyMode = ConcurrencyMode.Multiple, 
    InstanceContextMode = InstanceContextMode.Single)]
public class SubscriptionService : ISubscriptionService
{

    private readonly IOperationContext _context = null;
    private readonly object _syncRoot = new object ();
    private readonly List<ISubscriberCallback> _subscribers = 
        new List<ISubscriberCallback> ();

    // inject our dependency on operation context! this
    // decoupling actually makes sense - if we swap 
    // transport stacks we just require an implementation
    // that returns our requested callbacks!
    public SubscriptionService (IOperationContext context)
    {
        _context = context;
    }

    public void Subscribe ()
    {
        // ... ahhhh, that's much better!
        ISubscriberCallback subscriber = 
            _context.
            GetCallbackChannel<ISubscriberCallback> ();

        lock (_syncRoot)
        {
            if (!_subscribers.Contains (subscriber))
            {
                _subscribers.Add (subscriber);
            }
        }
    }

}

Now, if we were so inclined to test Subscribe above,

// test business logic without WCF! yay!
[TestMethod]
public void Test_Subscribe ()
{
    IOperationContext mockContext = null;
    // instantiate mock with expectations
    SubscriptionService service = new SubscriptionService (mockContext);
    service.Subscribe ();
    // verify results
}