Unit testing SOA – Testing your connection to a service

In response to a post on Jimmy Nilsson’s blog, I was asked to make a posting about testing a web service (or generic service for that matter) in an SOA architecture. My feeling is that the reason that services aren’t often tested is because making service calls is costly and makes your suite of tests for the services that you call take a very long time (compared to unit testing a local class). Anyway… on with the post:
 
    The Service Layer I am referring to is one as defined by Martin Fowler where a service is typically sits on top of and passes off all processing to an underlying layer, fulfilling its role as simply being an application boundary, nothing more. Therefore, if you test the underlying layer in your unit tests, you ask yourself, "Wait, I have this code that’s untested. I know that if it breaks that my application also breaks, therefore I should have a test for it… but how?"
 
Seeing as it’s simply a service, from the perspective of your application you have to treat it as a black box (consider that you are consuming a third party service where you don’t control the underlying implementation; you still want to be able to test your part of the application). Therefore, you are left with basically testing that your interface to the service (the service proxy) is still in line with the real service, making sure that each method that you are calling in the service still returns something (it doesn’t really matter what it is as you should be testing the calling mechanism separately to ensure that it successfully handles bad returned data [like null values etc.]).
 
I haven’t confirmed that this pseudocode compiles but it should serve to get the point across.
 
Consider this situation:
Calling Mechanism —-calls—-> Service Proxy —-calls—-> Service
 
To reduce coupling, enable you to mock out your service layer, and independently test your calling mechanism without having to set stuff up in your service layer, you have to build an interface and a factory.
 
A simple example, using an ASP.NET Web Service, here:
 
————————————– Interface
//(created to reduce coupling and to be able to test the CallingMechanism independant of the Service)
public interface IBlackBoxService {
    string GetFullUserNameFromUserId(string userId);
}
 
————————————– Service Proxy
[WebServiceBinding(Name="BlackBoxService", Namespace="http://www.ServiceProvider.com")]
public class BlackBoxServiceProxy : System.Web.Services.Protocols.SoapHttpClientProtocol, IBlackBoxService {
    public BlackBoxServiceProxy() {
    }
 
    [SoapDocumentMethod("http://www.ServiceProvider.com/BlackBoxService", RequestNamespace=" http://www.ServiceProvider.com", ResponseNamespace="http://www.ServiceProvider.com",  U se=SoapBindingUse.Literal, ParameterStyle=SoapParameterStyle.Wrapped
    public string GetFullUserNameFromUserId(string userId) {
        object[] results = this.Invoke("GetFullUserNameFromUserId", new object[0]);
        return ((string)results[0]);
    }
}
 
————————————– Service
public class BlackBoxService : WebService {
    public BlackBoxService()  { }
    [WebMethod]
    public string GetFullUserNameFromUserId(string userId) {
        … does something in here and returns something.. could be null, could be string.Empty, could be invalid, who knows. Ensure that your calling mechanism is tested to ensure it can handle anything that could be returned from this potentially hostile service.
    }
}
 
————————————– Service Proxy Factory
public class ServiceProxyFactory {
    public static readonly ServiceProxyFactory INSTANCE = new ServiceProxyFactory();
    private IBlackBoxService blackBoxService;
    private ServiceProxyFactory() {
        blackBoxService = new BlackBoxServiceProxy();
    }
 
    public void MockOutProxies() {
        blackBoxService = new MockBlackBoxServiceProxy();
    }
 
    public void ResetProxies() {
        blackBoxService = new BlackBoxServiceProxy();

    }

 
    public IBlackBoxServiceBlackBoxService BlackBoxService {
        get { return blackBoxService; }
    }
}
 
————————————– MockBlackBoxServiceProxy
public class MockBlackBoxServiceProxy : IBlackBoxService  {
    public const string USERNAME_THAT_RETURNS_NULL = "one";

    public const string USERNAME_THAT_RETURNS_NUMBERS = "two";

    public const string USERNAME_THAT_RETURNS_EMPTY_STRING = "three";

    public const string USERNAME_THAT_RETURNS_WORD_CHARACTERS = "four";

    public MockBlackBoxServiceProxy() { }
    public string GetFullUserNameFromUserId(userId) {
        string returnValue = null;
        switch(userId) {
            case USERNAME_THAT_RETURNS_NULL:
                returnValue = null;
                break;

            case USERNAME_THAT_RETURNS_NUMBERS:
                returnValue = "12313";
                break;
            case USERNAME_THAT_RETURNS_EMPTY_STRING:
                returnValue = string.Empty;
                break;
            case USERNAME_THAT_RETURNS_WORD_CHARACTERS:
                returnValue = "word string with spaces";
                break;
            default:
                break;

        }

        return returnValue;
    }
}
 
 
————————————– Calling Mechanism
public class CallingMechanism {
    public CallingMechanism() { }
    public string GetUserNameFromService(string userId) {
        return ServiceProxyFactory.INSTANCE.BlackBoxService.GetFullUserNameFromUserId(userId);
    }
}
 
….. and finally, the tests….

 
————————————– Service Proxy Test
[TestFixture]
public class BlackBoxServiceProxyTest {
    public BlackBoxServiceProxyTest() { }
    [SetUp]
    public void SetUp() {
        ServiceProxyFactory.INSTANCE.ResetProxies(); //Just to be sure.
    }
    [Test]
    public void GetFullUserNameFromUserIdTest() {
        //Note that this is a valid test (in this case),even though there is no assertion (in this case)
        //because all I am testing is to ensure that no exception was not thrown. Because this is a UNIT test
        //of the connection to the service, I only care that my proxy is still in line with the service I am calling.
        //The test for the Calling Mechanism should assert that it deals with the possible
        //values being returned.
        ServiceProxyFactory.INSTANCE.BlackBoxService.GetFullUserNameFromUserId(string.Empty);
    }
}

 
————————————– Calling Mechanism Test
[TestFixture]

public class CallingMechanismTest {
    private CallingMechanism callingMechanism = new CallingMechanism();
    public CallingMechanismTest() { }
    [SetUp]
    public void SetUp() {
        //I don’t want to test the connection to the service.
        //I only want to UNIT test the CallingMechanism
        ServiceProxyFactory.INSTANCE.MockOutProxies(); 
    }
    [Test]
    public void GetUserNameFromServiceCanHandleNull() {
        Assert.IsNull(callingMechanism.GetUserNameFromService(MockBlackBoxServiceProxy.USERNAME_THAT_RETURNS_NULL);
    }
    …. do tests of other posible return values
}
 

————————————– Service Proxy Factory Test (just for completeness)
[TestFixture]
public class ServiceProxyFactoryTest {
    public ServiceProxyFactoryTest() { }
    [SetUp]
    public void SetUp() {
        ServiceProxyFactory.INSTANCE.Reset();
    }
    [Test]
    public void RealProxyIsReturned() {
        Assert.AreEqual(typeof(BlackBoxServiceProxy), ServiceProxyFactory.INSTANCE.BlackBoxServiceProxy.GetType());
    }
    [Test]
    public void RealProxyIsReplacedWithMoxkProxy() {
        Assert.AreEqual(typeof(BlackBoxServiceProxy), ServiceProxyFactory.INSTANCE.BlackBoxServiceProxy.GetType());
        ServiceProxyFactory.INSTANCE .MockOutProxies();
        Assert.AreEqual(typeof(MockBlackBoxServiceProxy), ServiceProxyFactory.INSTANCE.BlackBoxServiceProxy.GetType());    }

}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s