📜 ⬆️ ⬇️

A bit about unit testing and external APIs in PHP

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

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

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:

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') { //   3- OAuth  $oauthKeys['token'] = $oauth_request_token; $oauthKeys['token_secret'] = $oauth_request_token_secret; } elseif ($authStatus == 'auth') { //   $oauthKeys['token'] = $oauth_access_token; $oauthKeys['token_secret'] = $oauth_access_token_secret; } $oauth = new Oauth1($oauthKeys); 

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); } 


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); } 


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]; // Guzzle returns some header values as 1-element array } else { $headers[$name] = $value; } } $response = htmlspecialchars($response, ENT_QUOTES); $headers_json = json_encode($headers); $data = "<?\n\$data = array('headers_json' => '$headers_json', \n'response' => '$response');"; $requestData = "<?\n\$reqdata = array('url' => '$requestUrl', \n'body' => '$requestBody');"; file_put_contents("path_of_your_choice/localResponses/{$filename}.inc", $data); file_put_contents("path_of_your_choice/localResponses/{$filename}_req.inc", $requestData); } 

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.

Source: https://habr.com/ru/post/260221/


All Articles