Unit testing is one of the integral parts of the development process, and it becomes more complicated and contradictory if the main task of your code is to send requests to external APIs and process responses. A lot of copies are broken about the topic, what should be the testing of the code tied to external sources, and where is the line between testing your own code and other people's API.
At this stage, developers have to decide which requests to send to a remote server, and which ones to simulate locally. There are many solutions for sending requests, and for their simulation. In my post, I’ll tell you how to do both using the Guzzle HTTP client.

')
A few words about the product.
Guzzle is an extensible HTTP client for PHP. He is in active development. Over the past year, two older versions. Version 4.0.0 was released in March 2014, and May 2015 brought the release of version 6.0.0. The transition between them can cause certain difficulties, because developers in each release change the namespace and some of the principles of work.
In addition, it may be difficult to combine various manuals, even written quite recently. However, if you properly google and read the native documentation, you can finally find an acceptable solution.
Installation
Guzzle is installed as a
Composer package. The installation file composer.json for our needs is as follows:
{ "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 tasks, you need to use 3-step OAuth authentication, so I had to stop at Guzzle 5.3. At the time of writing, this is the latest version that supports the plugin oauth-subsctiber. However, if you do not need OAuth, you can try to adapt the solution for version 6. *. Naturally, first check with the documentation.
The first steps
First of all, you will need to connect the Composer package startup file:
require_once "path_to_composer_files/vendor/autoload.php";
In addition, we need to specify which namespaces will be used by our scripts. In my case, different files are responsible for sending requests and saving the results. If necessary, you can combine them.
Sending requests and logging
Save and Simulate use GuzzleHttp\Subscriber\Mock; use GuzzleHttp\Message\Response; use GuzzleHttp\Stream\Stream;
Sending requests
To determine the current mode of operation, we use two global variables:
- $ isUnitTest determines whether the system is operating normally or in automatic testing mode;
- $ recordTestResults tells the system to save the data of all requests and responses.
OAuth procedures
Guzzle allows you to use the same code for OAuth authorization itself (according to different schemes), and for sending signed requests.
$oauthKeys = [ 'consumer_key' => OAUTH_CONSUMER_KEY, 'consumer_secret' => OAUTH_CONSUMER_SECRET, ]; if ($authStatus == 'preauth') {
The
OAUTH_CONSUMER_KEY and
OAUTH_CONSUMER_SECRET constants are a pair of keys provided by your API provider. Depending on the current authorization status, a token and its private key may be required. For more information about OAuth, you can refer to relevant sources (for example, the
OAuth Bible ).
HTTP client initialization
In this step, we determine whether we need to send a real request or receive a locally stored response.
if (empty($isUnitTest) || !empty($recordTestResults)) { $client = new Client(['base_url' => $apiUrl, 'defaults' => ['auth' => 'oauth']]); $client->getEmitter()->attach($oauth); } else { $mock = getResponseLocally($requestUrl, $requestBody); $client = new Client(); $client->getEmitter()->attach($mock); }
- $ apiUrl - the base path of your API
- 'defaults' => ['auth' => 'oauth'] is needed only if you send OAuth requests. The same is true for $ client-> getEmitter () -> attach ($ oauth);
- $ requestUrl - the full path of the request (including the base path)
- $ requestBody request body (may be empty)
I will describe the work of the
getResponseLocally () function a little later. If you want to add logging in development mode, enter another global variable
$ inDevMode and add the following code:
if ($inDevMode) { $log = new Logger('guzzle'); $log->pushHandler(new StreamHandler('/tmp/guzzle.log')); $subscriber = new LogSubscriber($log, Formatter::SHORT); $client->getEmitter()->attach($subscriber); }
Sending requests and receiving responses
At this stage we are ready to send the request. I have simplified the saving algorithm for myself and do not write down the HTTP response code. If you need it, it is easy to modify the code.
$request = $client->createRequest($method, $requestUrl, ['headers' => $requestHeaders, 'body' => $requestBody, 'verify' => false]); $output = new stdClass(); try { $response = $client->send($request, ['timeout' => 2]); $responseRaw = (string)$response->getBody(); $headers = $response->getHeaders(); } catch (Exception $e) { $responseRaw = $e->getResponse(); $headers = array(); } if ($recordTestResults) { saveResponseLocally($requestUrl, $requestBody, $headers, $responseRaw); }
- $ method - HTTP request method (GET, POST, PUT etc)
- $ requestHeaders - request headers (if needed)
- $ headers - response headers
- $ responseRaw - raw response (you can get the XML, JSON or whatever)
Saving and Simulating Responses
Local copies of responses can be saved to files or a database. Whichever option you choose, it is necessary to unambiguously match the request with the answer. I decided to use for this purpose MD5 hashes of the
$ requestUrl and
$ requestBody variables . The array of headers is parked in JSON and together with the response body is saved as a php file, which can be easily uploaded with
require () .
function saveResponseLocally ($requestUrl, $requestBody, $headers_source, $response) { if (!is_string($requestBody)) { $requestBody = print_r($requestBody, true); } $filename = md5($requestUrl) . md5($requestBody); $headers = array(); foreach ($headers_source as $name => $value) { if (is_array($value)) { $headers[$name] = $value[0];
In fact, for further work, you do not need to create and save
$ requestData . However, this feature can be useful for debugging.
As I mentioned, I do not save the response code, so I create all the answers with code 200. If your error handling system requires a specific HTTP code, you can easily add the appropriate feature.
function getResponseLocally ($requestUrl, $requestBody) { if (!is_string($requestBody)) { $requestBody = print_r($requestBody, true); } $filename = md5($requestUrl) . md5($requestBody) . '.inc'; if (file_exists("path_of_your_choice/localResponses/{$filename}")) { require("path_of_your_choice/localResponses/{$filename}"); $data['headers'] = (array)json_decode($data['headers_json']); $mockResponse = new Response(200); $mockResponse->setHeaders($data['headers']); $separator = "\r\n\r\n"; $bodyParts = explode($separator, htmlspecialchars_decode($data['response']), ENT_QUOTES); if (count($bodyParts) > 1) { $mockResponse->setBody(Stream::factory($bodyParts[count($bodyParts) - 1])); } else { $mockResponse->setBody(Stream::factory(htmlspecialchars_decode($data['response']))); } $mock = new Mock([ $mockResponse ]); return $mock; } else { return false; } }
In conclusion...
I described only one of the options for simulating API responses and saving time in unit testing (in my case, testing with local answers takes about 10-20 times less time than “combat” requests). Guzzle provides a
couple more
ways to solve this problem.
If you need more complex testing, you can even create a local API simulator that will create the answers you need. Whichever way you choose, you can always be sure that you save a lot of time and avoid sending too many requests to your API partners.
