Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Need to Test a Service that Uses CURL in Laravel 5.1

I built a service in for my Laravel 5.1 API that searches YouTube. I am trying to write a test for it but am having trouble figuring out how to mock the 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 abstract the functionality from a controller. I then used Mockery to test the controller. Now I need to figure out how to test the service above. Any help is appreciated.

like image 976
WebDev84 Avatar asked Sep 25 '22 23:09

WebDev84


1 Answers

Need to say, your class is not designed for isolated unit testing because of hardcoded curl_* methods. For make it better you have at least 2 options:

1) Extract curl_* functions calls to another class and pass that 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 mock CurlCaller class. There is a lot of ready solutions that abstracts network. For example, Guzzle is great

2) Another option is to extract curl_* calls to protected method and mock that 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 method callUrl. But first, lets put expected 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 test and... OMG FAILURE!!1 You have typos in extractVideo method, should be $item instead of $items. Lets fix it

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

OK, now it pass.


If you want to test your class with call to Youtube API you just need to create normal Youtube class.


BTW, there is php-youtube-api lib, which has providers for laravel 4 and laravel 5, also it has tests

like image 82
Nikita U. Avatar answered Oct 11 '22 17:10

Nikita U.