/ 
1 Add click sound timing 2 Add typing sound timing Finished
© 2018 Zipier Ltd. All rights reserved.
PlansPricingLogin
2015-06-09 Unit testing and external APIs
 

2015-06-09 Unit testing and external APIs

Unit testing is an important part of the development process. Which becomes harder and more contradictory if the main purpose of your code is running external API calls and processing the results.

At Zipier, not only do we calculate paystubs for you (and your employees), but also prepare all the related accounting data. Some of the popular online accounting systems allow you to post this data directly into your accounting platform, via API. For me, as the developer, it means dealing with a lot of different APIs, and developing / testing code that relies on external APIs.

When you've got a lot of queries you must decide which ones must be sent remotely and which ones should remain local. There're different ways to get the real requests and to fake them. We decided to do this using Guzzle.

Just a few words about it. Guzzle is an extendable HTTP client for PHP. It's under active development. They produce approximately two major versions per year.

Sometimes switching to a newer version can cause problems because they're constantly changing namespaces, this is what I've experienced with the last two updates.

Version 4.0.0 was released in March 2014 and May 2015 brought us version 6.0.0. So different "how-tos" written just a year and half ago become useless. Also it could be hard to combine different "how-tos".

Anyway, if you spend some time Googling, readingScore docs and experimenting you may find a suitable solution for your needs.

 

Guzzle is installed as Composer package. composer.json file for your needs will look like this:

{
    "name": "our-guzzle-test",
    "description": "Guzzle setup API testing",
    "minimum-stability": "dev",
    "require": {
        "guzzlehttp/guzzle": "5.*",
        "guzzlehttp/log-subscriber": "*",
        "monolog/monolog": "*",
        "guzzlehttp/oauth-subscriber": "*"
    }
}

For some of our API integrations we need to use OAuth so we decided to focus on Guzzle 5.3. It's the latest version that supports oauth-subsctiber plugin at the moment of writing this post. You may try to reproduce the same with 6.* if you don't need OAuth. Don't forget to consult the latest docs.

 

First thing you need to do is require your composer autoload file:

require_once "path_to_composer_files/vendor/autoload.php";

In our case we use separate files for making queries and saving/mocking them. You can combine them if you need to.

Sending queries and logging:

use GuzzleHttp\Client;
use GuzzleHttp\Subscriber\Oauth\Oauth1;
use GuzzleHttp\Subscriber\Log\LogSubscriber;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use GuzzleHttp\Subscriber\Log\Formatter;

If you don't need to do logging you should skip the last 4 lines.

Saving/mocking part:

use GuzzleHttp\Subscriber\Mock;
use GuzzleHttp\Message\Response;
use GuzzleHttp\Stream\Stream;
 

To determine the current mode we use 2 variables:

  • $isUnitTest determines if the system is running in manual mode or under unit testing;
  • $isRecordingTestResults tells the system if it has to save every request and response data.

OAuth routine

Some of our APIs use a 3-legged OAuth scheme. We use the same function to send requests during authentication and querying.

$oauthKeysArr = ['consumer_key' => OAUTH_CONSUMER_KEY, 'consumer_secret' => OAUTH_CONSUMER_SECRET];
if ($authStatus == 'preauth') { // "3rd leg" of OAuth connection
    $oauthKeysArr['token']        = $oauth_request_token;
    $oauthKeysArr['token_secret'] = $oauth_request_token_secret;
} elseif ($authStatus == 'auth') { // normal query
    $oauthKeysArr['token']        = $oauth_access_token;
    $oauthKeysArr['token_secret'] = $oauth_access_token_secret;
}
$oauthObj = new Oauth1($oauthKeysArr);

OAUTH_CONSUMER_KEY and OAUTH_CONSUMER_SECRET is the key pair issued for your app by the API provider. Depending on the current authentication status you may or may not have to provide the token and secret keys. Please refer to OAuth Bible for more details about tokens and their types.

Initiating HTTP client

This is actually the step when we decide if we've got to run a real query or read a local mock response.

if (!$isUnitTest || $isRecordingTestResults) {
    $clientObj = new Client(['base_url' => $apiUrl, 'defaults' => ['auth' => 'oauth']]);
    $clientObj->getEmitter()->attach($oauthObj);
} else {
    $mockObj   = getResponseLocally($requestUrl, $requestBodyText);
    $clientObj = new Client();
    $clientObj->getEmitter()->attach($mockObj);
}
  • $apiUrl is a base path for your API
  • 'defaults' => ['auth' => 'oauth'] is only needed if you do your queries with OAuth. Same for $client->getEmitter()->attach($oauth);
  • $requestUrl is a full request path
  • $requestBodyText is a request body (can be empty)

I will explain how does getResponseLocally work later.

If you'd like to do some logging in dev mode you should also add this code:

if ($isDevMode) {
    $logObj = new Logger('guzzle');
    $logObj->pushHandler(new StreamHandler('/tmp/guzzle.log'));
    $subscriberObj = new LogSubscriber($logObj, Formatter::SHORT);
    $client->getEmitter()->attach($subscriberObj);
}

Sending request and getting responses

At this point we're ready to send the request. I've simplified the saving algorithm and decided not to store the response code. But you can adjust saving and getting functions to do so.

$requestObj = $client->createRequest($method, $requestUrl, ['headers' => $requestHeadersArr, 'body' => $requestBodyText, 'verify' => (bool) 0]);
$outputObj  = new stdClass();
try {
    $responseObj       = $client->send($requestObj, ['timeout' => 2]);
    $responseRawText   = (string) $responseObj->getBody();
    $headersArr        = $responseObj->getHeaders();
} catch (Exception $e) {
    $responseRawText   = $e->getResponse();
    $headersArr        = array();
}
if ($isRecordingTestResults) {
    saveResponseLocally($requestUrl, $requestBodyText, $headersArr, $responseRawText);
}
  • $method - HTTP request method (GET, POST, PUT etc)
  • $requestHeadersArr - request headers (if needed)
  • $headersArr - response headers
  • $responseRaw - raw response (you may get XML, JSON or whatever else as a response but you need to save it before decoding)
 

Local requests can be saved to files or database. Either way you choose to save the responses, you'll need to match the request with the response exactly. I decided to use MD5 hash of $requestUrl and $requestBodyText for this purpose. Headers array is packed to JSON. Headers array and raw response are saved as a php file that can be easily loaded with require.

function saveResponseLocally ($requestUrl, $requestBodyText, $headers_source, $responseRawText) {
    if (!is_string($requestBodyText)) {
        $requestBodyText = print_r($requestBodyText, true);
    }
    $fileName = md5($requestUrl) . md5($requestBodyText);
    $headersArr  = array();
    foreach ($headers_source as $headerName => $headerValueMixed) {
        if (is_array($value)) {
            $headersArr[$headerName] = $headerValueMixed[0]; // Guzzle returns some header values as 1-element array.
        } else {
            $headersArr[$headerName] = $headerValueMixed;
        }
    }
    $responseRawText     = htmlspecialchars($responseRawText, ENT_QUOTES);
    $headersJson = json_encode($headersArr);
    $dataArr      = "< ?\n\$dataArr = array('headers_json' => '$headersJson', \n'response' => '$responseRawText');";
    $requestDataText  = "< ?\n\$reqdataArr = array('url' => '$requestUrl', \n'body' => '$requestBodyText');";
    file_put_contents("path_of_your_choice/localResponses/{$fileName}.inc", $dataArr);
    file_put_contents("path_of_your_choice/localResponses/{$fileName}_req.inc", $requestDataText);
}

You don't really need to create $requestDataText and save it. Although it can be useful for debugging.

I don't save the exact response code, I mock all responses with HTTP status 200. You can easily save and recall the proper response code if you really need it.

function getResponseLocally ($requestUrl, $requestBodyText) {
    if (!is_string($requestBodyText)) {
        $requestBodyText = print_r($requestBodyText, true);
    }
    $fileName = md5($requestUrl) . md5($requestBodyText) . '.inc';
    if (file_exists("path_of_your_choice/localResponses/$fileName")) {
        require("path_of_your_choice/localResponses/$fileName");
        $dataArr['headers'] = (array)json_decode($dataArr['headers_json']);
        $mockResponseObj    = new Response(200);
        $mockResponseObj->setHeaders($dataArr['headers']);
        $separatorStr = "\r\n\r\n";
        $bodyPartsArr = explode($separatorStr, htmlspecialchars_decode($dataArr['response']), ENT_QUOTES);
        if (count($bodyPartsArr) > 1) {
            $mockResponseObj->setBody(Stream::factory($bodyPartsArr[count($bodyPartsArr) - 1]));
        } else {
            $mockResponseObj->setBody(Stream::factory(htmlspecialchars_decode($dataArr['response'])));
        }
        $mockObj = new Mock([
            $mockResponseObj
        ]);
        return $mockObj;
    } else {
        return false;
    }
}
 

I've described just one way to mock API responses and minimize the time spent on unit testing. Guzzle provides a couple of other ways to solve this problem.

If you need more complicated testing you can even create a local API simulator that will fake responses for you. Whichever solution you choose you can be sure you'll save time and avoid sending excessive requests to your partners.

Share this:

Was this article helpful?

Yes No

© 2018 Zipier Ltd. All rights reserved. www.zipier.com/home/Blog/2015-06-09_Unit_testing_and_external_APIs