Unit Testing Code which Consumes SOAP Services
One of the trickiest aspects of unit testing or Test Driving an application's code is testing those parts of the system which depend on an external system, such as a database or a SOAP service.
In this post I'll outline an approach to testing a class which happens to communicate with a third-party SOAP service using PHP's built-in SoapClient
class. Hopefully, the principles involved will be applicable to the more general case of testing code which relies on an external system.
The Problem
Imagine a class which talks to an external SOAP service using PHP's built-in SoapClient
. Now imagine trying to unit test that class without calling methods on the external service.
We don't need to look far for an example: when consuming a SOAP service it is not uncommon to "wrap" that service, by placing all the code that talks to it in a dedicated class. That way, the mechanics and details of working with the service, and even the fact that SOAP is the communication method of choice, are encapsulated and hidden from the rest of the application. In addition the wrapper class can take care of some of the error handling, reformat SOAP responses into something more convenient, such as a straightforward array
or DTO, and so forth.
Let's look at some code. Imagine we're using a SOAP service which translates text from English to French. A simplified wrapper class may look something like the following. One might add PHPDoc blocks and a touch more error-handling, but let's keep it simple for now:
<?php
class TranslationService
{
private $_client;
public function __construct()
{
$this->_client = new SoapClient(
'http://example.com/soap.wsdl');
}
public function translate($englishtext)
{
$response = $this->_client->translateToFrench($englishtext);
if ( $response->success ) {
return $response->translation;
}
return FALSE;
}
}
Example client code for that class would look something like so:
<?php
$service = new TranslationService();
$french = $service->translate('Some English text');
Can you spot why the TranslationService
class is hard to unit test? That line of code that calls new SoapClient()
is the problem: it is impossible to run the code in this class without connecting to the remote SOAP service and calling its methods. If the service is slow or unstable, repeated testing is going to be impractical. In other cases, there may be a financial cost involved in using that service, or - don't laugh, because this really happens - the SOAP service may not be built yet.
What we really want to do is have some way of swapping out the SOAP calls, perhaps by replacing the SoapClient
with some kind of dummy object. However, that innocent-looking new SoapClient()
creates a concrete dependency between the wrapper class and the SoapClient
class. In turn, the code in the wrapper class is tightly coupled to the external service.
As a general principle, every time you use the new
operator, you create a concrete dependency, and greatly decrease the flexibility of your code, so it's worthwhile to stop and figure out if there is a better way. There typically is, and this is why design pattern books are stuffed with examples of object-creational and other patterns which will help you avoid it.
So before we can unit test this code, we need to make it testable, and we'll achieve that using a little Inversion of Control, and the Strategy pattern. Fortunately, that's going to be a lot simpler than it sounds.
Making the Code Testable
To make this code testable, we need to break the dependency on SoapClient
. We'll do that by removing the hard-coded new SoapClient()
call, and instead pass the client object into TranslationService
as a parameter to its constructor:
<?php
class TranslationService
{
private $_client;
public function __construct($client)
{
$this->_client = $client;
}
public function translate($englishtext)
{
$response = $this->_client->translateToFrench($englishtext);
if ( $response->success ) {
return $response->translation;
}
return FALSE;
}
}
The revised client code will look like the following:
<?php
$client = new SoapClient('http://example.com/soap.wsdl');
$service = new TranslationService($client);
$french = $service->translate('Some English text');
Admittedly, it appears as though there is a down side to this. The client code is more complex, and is now tightly coupled to both TranslationService
and the fact that it uses SOAP! Again, this is because of careless use of the new
operator. In practice we would do all of that object creation within a Factory class or method. The Factory pattern is extensively documented, and beyond the scope of this post, so I won't dwell on it.
So what have we gained? We've decoupled TranslationService
from SoapClient
by removing a hard-coded dependency. This means that we can now easily test TranslationService
using a technique known as "mocking".
Mocking the SOAP Client
Mock objects are again well documented, but in summary, a Mock is an object that can pretend to be another object. You can tell it how to behave, and subsequently ask it to report on what happened to it - the dual roles of actor and critic
, in Marcus Baker's words. In our case we'll be mocking SoapClient
, which will allow us to completely prevent any SOAP calls to the third-party translation service being made during our test run.
Pretty much every xUnit-style testing framework ships with built-in support for Mock objects, and for the record, I'm going to be using SimpleTest here.
Here's a first attempt at that:
<?php
class TranslationServiceTestCase extends UnitTestCase
{
public function testTranslation()
{
Mock::generate('SoapClient');
$mocksoapclient = new MockSoapClient();
$dummyresponse = new stdClass();
$dummyresponse->success = TRUE;
$dummyresponse->translation = "Ceci n'est pas une pipe";
$englishtext = 'Some English text';
$mocksoapclient->setReturnValue('translateToFrench'
, $dummyresponse
, array($englishtext));
$translationservice = new TranslationService($mocksoapclient);
$frenchtext = $translationservice->translate($englishtext);
$this->assertEqual($frenchtext, $dummyreponse->translation);
}
}
Since we now pass the SoapClient
into TranslationService
it is trivial to pass "something else" instead, in this case a MockSoapClient
. We can control the Mock's behaviour, allowing us to simulate any possible behaviour that the real SOAP service might display, and assert that our code does the right thing in all cases. This is a big step forward.
Except let's try to run that test case. Here is SimpleTest's output:
Argh. Since PHP's built in SoapClient
has no translateToFrench()
method, neither does the MockSoapClient
which SimpleTest generated for us. Consequently, we cannot set the return value of that method, and our testing plan appears to be on the rocks.
Luckily we can resort to PHP's magic __call()
method. The syntax is slightly more arcane, but the win is huge:
<?php
class TranslationServiceTestCase extends UnitTestCase
{
public function testTranslation()
{
// ...code omitted for brevity...
// This fails badly
/* $mocksoapclient->setReturnValue('translateToFrench'
, $dummyresponse
, array($englishtext)); */
// This works nicely
$mocksoapclient->setReturnValue('__call'
, $dummyresponse
, array('translateToFrench', array($englishtext)));
$translationservice = new TranslationService($mocksoapclient);
$frenchtext = $translationservice->translate($englishtext);
// ...code omitted for brevity...
}
}
That works like a charm, and we are now fully able to test our TranslationService
class, completely independently of the external service.
Conclusions
We've looked at a very specific case - that of testing a SOAP service wrapper class, written in PHP, with SimpleTest. However, the approach is more generally applicable, and back in the office we use this exact technique to test PHP code which calls Java methods over RMI, as well as classes which query databases or fetch data from URLs over HTTP.
There's a big practical gain in terms of testability, but that all stems from the much more general principle of achieving flexible, decoupled code by appropriate use of object-orientation and design patterns. And of course, a cautious approach to using the new
operator!
Simon Harris
That's irrelevant, really.
I think you're questioning why the class exists in the first place, rather than why you want to make it testable? In which case I'll have to ask you to take a leap of faith that TranslationService - which is nothing more than example code - does "stuff". The nature of that stuff is really beyond the point of this post, but I do hint at it:
"some of the error handling, reformat SOAP responses into something more convenient...and so forth."
Point being, it has behaviour that you want to test.
Ciaran McNulty
The general point about testing SoapClients is very interesting, but I've a slight worry that in taking the instancing of the SoapClient outside your class, you'd be losing some of that valuable abstraction?
Maybe I'm making an assumption that you were trying to hide the fact it was a SOAP service? Mind you I've no better solution!
Simon Harris
I do see where you're coming from on that, and we do definitely want to hide the SOAPness of the service. So one thing that maybe I shouldn't have glossed over is the Factory. That plays an important role in all of this, and the rest of the app should never, ever be new
ing up TranslationService (and by implication, SoapClient). Doing that all over the place would suck in exactly the way I suspect you're keen to avoid.
Unfortunately PHP gives you limited means to enforce that. In Java, say, I'd imagine all the service wrappers to be in the same package, and their constructors marked package
. The package's only public methods would be the Factory methods, so there would be no way for the rest of the app to new
a TranslationService.
[Edit] I should also have mentioned that the "client code" could actually be inside TranslationService itself anyway, in a static getInstance()
type method. But I'm splitting hairs about factories now, which is kind of what I wanted to avoid!
Charles Schaefer
Just a feedback... PHPUnit has a Mock builder that creates Mock Objects from a WSDL file: http://www.phpunit.de/manual/current/en/test-do...GoogleTest.php.
Ciaran McNulty
In your 'revised code' above, I'm not sure what you're gaining from the TranslationService's abstraction, if you're having to pass it a SoapClient instance.