Mocking SoapClient

Posted 2012-12-21
Written by Matt Frost
Category code

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));
Mocking the call method twice does what you might expect, the bottom one takes precedence over the top one and 'getThings' is no longer mocked. I did a little bit of digging and came across the logicalOr method as a way of providing different parameter sets to the same method. Here's how you'd do the example above correctly.
$mock->expects($this->any())
    ->method('__call')
    ->with($this->logicalOr(
        'getThings',array($apiKey),
        'getOtherThings',array($apiKey))
        )
    ->will($this->returnCallback(array($this,'callback')));
You can see the logicalOr and how it's used, now when ever getThings or getOtherThings is called in my test scenario it will run a callback. So let's talk about the callback, the callback is really helpful and it's pretty simple to understand. It will take in the method that set it off, by executing a pretty simple switch statement, you can provide the return value for each mocked method that gets called. Here's a really quick example of what this would look like...
public function callback($action)
{
    switch($action) {
        case 'getThings':
            return true;
            break;
        case 'getOtherThings':
            return false;
            break;
    }
}
I thought this was pretty awesome...

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));
From there we can use expects(), method(), will() and with() to set parameters, return values for a method as you normally would. Doing that with your wrapper, will allow you to return what you'd expect from the wrapper itself. This is easier to do in my opinion, but if you've got helper methods (for example to filter or sort the data coming back), you have to remember to mock those as well. It helps to have the method you're mocking open in a split screen with your test, you can see everything you need to mock.

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

Gravatar
Joseph Crawford

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.

Posting comments after has been disabled.