Need to test a service using CURL in Laravel 5.1

I built a service for my Laravel 5.1 API, which is looking for YouTube. I'm trying to write a test for it, but it's hard for me to understand how to mock functionality. Below is the service.

class Youtube { /** * Youtube API Key * * @var string */ protected $apiKey; /** * Youtube constructor. * * @param $apiKey */ public function __construct($apiKey) { $this->apiKey = $apiKey; } /** * Perform YouTube video search. * * @param $channel * @param $query * @return mixed */ public function searchYoutube($channel, $query) { $url = 'https://www.googleapis.com/youtube/v3/search?order=date' . '&part=snippet' . '&channelId=' . urlencode($channel) . '&type=video' . '&maxResults=25' . '&key=' . urlencode($this->apiKey) . '&q=' . urlencode($query); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); curl_close($ch); $result = json_decode($result, true); if ( is_array($result) && count($result) ) { return $this->extractVideo($result); } return $result; } /** * Extract the information we want from the YouTube search resutls. * @param $params * @return array */ protected function extractVideo($params) { /* // If successful, YouTube search returns a response body with the following structure: // //{ // "kind": "youtube#searchListResponse", // "etag": etag, // "nextPageToken": string, // "prevPageToken": string, // "pageInfo": { // "totalResults": integer, // "resultsPerPage": integer // }, // "items": [ // { // "kind": "youtube#searchResult", // "etag": etag, // "id": { // "kind": string, // "videoId": string, // "channelId": string, // "playlistId": string // }, // "snippet": { // "publishedAt": datetime, // "channelId": string, // "title": string, // "description": string, // "thumbnails": { // (key): { // "url": string, // "width": unsigned integer, // "height": unsigned integer // } // }, // "channelTitle": string, // "liveBroadcastContent": string // } // ] //} */ $results = []; $items = $params['items']; foreach ($items as $item) { $videoId = $items['id']['videoId']; $title = $items['snippet']['title']; $description = $items['snippet']['description']; $thumbnail = $items['snippet']['thumbnails']['default']['url']; $results[] = [ 'videoId' => $videoId, 'title' => $title, 'description' => $description, 'thumbnail' => $thumbnail ]; } // Return result from YouTube API return ['items' => $results]; } } 

I created this service to distract functionality from the controller. Then I used Mockery to test the controller. Now I need to figure out how to check the above service. Any help is appreciated.

+5
source share
2 answers

I must say that your class is not intended for isolated unit tests due to the hard-coded curl_* methods. To make it better, you have at least 2 options:

1) Extract curl_* function calls to another class and pass this class as a parameter

 class CurlCaller { public function call($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); curl_close($ch); return $result; } } class Youtube { public function __construct($apiKey, CurlCaller $caller) { $this->apiKey = $apiKey; $this->caller = $caller; } } 

Now you can easily make fun of the CurlCaller class. There are many ready-made solutions that abstract the network. For example, Guzzle is excellent

2) Another option is to extract the curl_* calls of the protected method and mock this method. Here is a working example:

 // Firstly change your class: class Youtube { // ... public function searchYoutube($channel, $query) { $url = 'https://www.googleapis.com/youtube/v3/search?order=date' . '&part=snippet' . '&channelId=' . urlencode($channel) . '&type=video' . '&maxResults=25' . '&key=' . urlencode($this->apiKey) . '&q=' . urlencode($query); $result = $this->callUrl($url); $result = json_decode($result, true); if ( is_array($result) && count($result) ) { return $this->extractVideo($result); } return $result; } // This method will be overriden in test. protected function callUrl($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $result = curl_exec($ch); curl_close($ch); return $result; } } 

Now you can mock the callUrl method. But first, let's assume the api response to fixtures/youtube-response-stub.json file.

 class YoutubeTest extends PHPUnit_Framework_TestCase { public function testYoutube() { $apiKey = 'StubApiKey'; // Here we create instance of Youtube class and tell phpunit that we want to override method 'callUrl' $youtube = $this->getMockBuilder(Youtube::class) ->setMethods(['callUrl']) ->setConstructorArgs([$apiKey]) ->getMock(); // This is what we expect from youtube api but get from file $fakeResponse = $this->getResponseStub(); // Here we tell phpunit how to override method and our expectations about calling it $youtube->expects($this->once()) ->method('callUrl') ->willReturn($fakeResponse); // Get results $list = $youtube->searchYoutube('UCSZ3kvee8aHyGkMtShH6lmw', 'php'); $expected = ['items' => [[ 'videoId' => 'video-id-stub', 'title' => 'title-stub', 'description' => 'description-stub', 'thumbnail' => 'https://i.ytimg.com/vi/stub/thimbnail-stub.jpg', ]]]; // Finally assert result with what we expect $this->assertEquals($expected, $list); } public function getResponseStub() { $response = file_get_contents(__DIR__ . '/fixtures/youtube-response-stub.json'); return $response; } } 

Run the test and ... OMG FAILURE !! 1 You have typos in the extractVideo method, there should be $item instead of $items . Let's fix it

 $videoId = $item['id']['videoId']; $title = $item['snippet']['title']; $description = $item['snippet']['description']; $thumbnail = $item['snippet']['thumbnails']['default']['url']; 

OK, now he is passing.


If you want to test your class when calling the Youtube API, you just need to create a normal Youtube class.


By the way, there is php-youtube-api lib that has providers for laravel 4 and laravel 5, there are also tests

+3
source

If you change the code in which the CURL calls are made, this is not an option, it can still be executed, but it is not very.

This solution assumes that the code that creates the CURL call bases the destination URL on an environment variable. The bottom line is that you can redirect the call back to your application, to the end point where the output can be controlled by your test. Since the instance of the application in which the test is being performed is actually different from the instance access to which when calling CURL causes a reversal, the way we solve the problems of defining the scope to allow the test to control the output through the forever cache, which records your dummy data to an external file that is accessed at runtime.

  • As part of the test, change the value of the environment variable responsible for the putenv("SOME_BASE_URI=".config('app.url')."/curltest/") call domain using: putenv("SOME_BASE_URI=".config('app.url')."/curltest/")

Since phpunit.xml usually sets the default value of CACHE_DRIVER to an array that is not constant, you will have to put this in your test to change it to file .

 config(['cache.default' => 'file']); 
  1. Create a new class in your tests folder, which will be responsible for returning this answer when the request meets a custom set of criteria:

    use Illuminate \ Http \ Request;

    class ResponseFactory {

     public function getResponse(Request $request) { $request = [ 'method' => $request->method(), 'url' => parse_url($request->fullUrl()), 'parameters' => $request->route()->parameters(), 'input' => $request->all(), 'files' => $request->files ]; $responses = app('cache')->pull('test-response', null); $response = collect($responses)->filter(function (array $response) use ($request) { $passes = true; $response = array_dot($response); $request = array_dot($request); foreach ($response as $part => $rule) { if ($part == 'response') { continue; } $passes &= is_callable($rule) ? $rule($request[$part]) : ($request[$part] == $rule); } return $passes; })->pluck('response')->first() ?: $request; if (is_callable($response)) { $response = $response($request); } return response($response); } /** * This uses permanent cache so it can persist between the instance of this app from which the test is being * executed, to the instance being accessed by a CURL call * * @param array $responses */ public function setResponse(array $responses) { app('cache')->forever('test-response', $responses); } 

    }

Since this is located in the tests folder and not in the App namespace, be sure to add it to the auto-load.classmap your composer.json file and run composer dumpautoload;composer install on the command line. Also, this is the use of a custom helper function:

 if (!function_exists('parse_url')) { /** * @param $url * @return array */ function parse_url($url) { $parts = parse_url($url); if (array_key_exists('query', $parts)) { $query = []; parse_str(urldecode($parts['query']), $query); $parts['query'] = $query; } return $parts; } } 
  1. Add endpoints to your routes for testing purposes only. (Unfortunately, placing $this->app->make(Router::class)->match($method, $endpoint, $closure); as far as I can tell from your test). Route::post('curltest/{endpoint?}', function (Illuminate\Http\Request $request) { return app(ResponseFactory::class)->getResponse($request); }); Route::get('curltest/{endpoint?}', function (Illuminate\Http\Request $request) { return app(ResponseFactory::class)->getResponse($request); }); Route::put('curltest/{endpoint?}', function (Illuminate\Http\Request $request) { return app(ResponseFactory::class)->getResponse($request); }); Route::patch('curltest/{endpoint?}', function (Illuminate\Http\Request $request) { return app(ResponseFactory::class)->getResponse($request); }); Route::delete('curltest/{endpoint?}', function (Illuminate\Http\Request $request) { return app(ResponseFactory::class)->getResponse($request); }); You can even wrap this in an if block if you want what config('app.debug') == true will do first.

  2. Customize the content of the responses to display an endpoint that is supposed to offer a specific response value. Put something like this in your test. app(ResponseFactory::class)->setResponse([[ 'url.path' => "/curltest/$curlTargetEndpont", 'response' => 'success' ]]);

0
source

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


All Articles