Mocking SoapClient
The concept of mocking web services for testability took a little while to sink in for me. A big part of it was that my job doesn't see me consuming web services all that often, but I had an opportunity to give it a shot with SOAP. I found that I learned a lot more about testing in general having worked through this. I used SoapClient and wrapped it, so here's a little bit about some of things I learned. Hopefully you don't have to work with SOAP, but if you do you can test it pretty easily.
Constructor
The object of testing this code is that we don't have to access web service at all, you shouldn't need your API key and you shouldn't need the path to your WSDL either, if you are actually connecting to the web service then you're doing it wrong. I worked with trying to provide a dummy WSDL and create the mock that way. I ended up getting a bunch of errors, and then I remembered that you can blow up the original constructor...long story short, with SoapClient, that's what you should do.
$mock = $this->getMockBuilder('SoapClient') ->disableOriginalConstructor() ->getMock();
Using the __call method
The next issue that I ran into was with using the __call method, I quickly learned that if you have to mock different calls in the same method, you can't just redefine everything. I know that probably sounds a bit confusing...so here's an example of what DOESN'T work
$mock is created above $mock->expects($this->any()) ->method('__call') ->with('getThings',array($apiKey)) ->will($this->returnValue(true)); $mock->expects($this->any()) ->method('__call') ->with('getOtherThings',array($apiKey)) ->will($this->returnValue(false));
$mock->expects($this->any()) ->method('__call') ->with($this->logicalOr( 'getThings',array($apiKey), 'getOtherThings',array($apiKey)) ) ->will($this->returnCallback(array($this,'callback')));
public function callback($action) { switch($action) { case 'getThings': return true; break; case 'getOtherThings': return false; break; } }
Wrapping
Most people will prefer to wrap the SOAP calls and have them make a little bit more sense for their application, mocking your wrapper is a good way to bypass mocking the SoapClient. This will be a little bit of a refresh on mocking stuff; so let's assume this: we have a constructor that takes an API key and a SoapClient instance, we have a getThings method and a getOtherThings method that just return JSON data. You can use a file with dummy JSON data or create a helper function to provide this information, and you should. So let's define the mock and the methods we're going to mock.
$mock= $this->getMock('SoapWrapper', array('__construct','getThings','getOtherThings'), array($apiKey,$soap));
Exceptions
Testing exceptions is a step that is normally passed over or forgotten. In my example, I threw my exceptions from the wrapper and caught them when I called the methods. I had some code to display the Exception messages in a jQuery dialog, so knowing that SoapFault extends Exception, I could catch all the exceptions that were thrown and get them displayed to the user somewhat gracefully. At any time, the web service can be down, your API key can stop working; so you've got to be prepared to handle all those exceptions, testing for them forces you to think about the scenarios where you could possibly have fail points and forces you to strategize a way to handle them well.
It was an interesting exercise and I felt like I learned a lot, at the end of the day my wrapper contained 24 Tests and 77 Assertions, most of the tests were tests for Exceptions. Hope this helpful, let me know what you think.
Comments
2012-12-21
Matt,
This was a very well written, interesting and informative read. I will keep this in Pocket for future use when I have to work with SOAP.