2015-12-10 59 views
5

我爲我的Laravel 5.1 API構建了一項服務,用於搜索YouTube。我正在爲它編寫一個測試,但在解決如何模擬功能時遇到困難。以下是服務。需要測試在Laravel 5.1中使用CURL的服務

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

我創建了此服務以從控制器中抽象該功能。然後我用Mockery來測試控制器。現在我需要弄清楚如何測試上面的服務。任何幫助表示讚賞。

回答

3

需要說的是,由於硬編碼的curl_*方法,您的班級不是爲隔離單元測試而設計的。爲了讓它更好的至少有2種選擇:

1)提取curl_*函數調用另一個類,並通過該類作爲參數

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

現在您可以輕鬆模擬CurlCaller類。 有很多現成的解決方案可以抽象出網絡。例如,Guzzle很好

2)另一個選項是提取curl_*調用受保護的方法並模擬該方法。這裏是一個工作的例子:

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

現在你可以模擬方法callUrl。但首先,讓我們把預期的API響應放到fixtures/youtube-response-stub.json文件中。

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

運行測試和...... OMG !!故障1你有extractVideo方法錯別字,應該是$item而不是$items。讓我們修復它

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

好的,現在它通過。


如果你想通過調用Youtube API來測試你的課程,你只需要創建一個普通的Youtube類。


順便說一句,存在php-youtube-api lib中,其具有用於laravel 4和5 laravel提供商,還具有測試

+0

非常感謝! – WebDev84

0

如果改變其中CURL呼叫所作的代碼是不是一種選擇,它可以仍然完成,但它並不漂亮。

此解決方案假定進行CURL調用的代碼將其目標URL基於環境變量。這裏的要點是,您可以將呼叫重定向到您自己的應用程序,也可以將輸出重定向到您的測試可以控制輸出的端點。由於執行測試的應用程序實例實際上與CURL調用掉頭時訪問的實例不同,因此我們處理範圍問題以允許測試控制輸出的方式是通過forever緩存,它將您的虛擬數據記錄到可在運行時訪問的外部文件。

  1. 內的測試,使用改變的環境變量負責捲曲呼叫的域的值: putenv("SOME_BASE_URI=".config('app.url')."/curltest/")

由於phpunit.xml通常設置默認CACHE_DRIVERarray,這是永久性的,你必須在測試中把它改回file

config(['cache.default' => 'file']); 
  • 創建tests文件夾中的一個新的類,將負責在請求滿足一組可配置的標準返回給定的響應:

    使用照亮\ HTTP \請求;

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

    }

  • 由於這是在tests文件夾,而不是App命名空間下,請務必將其添加到您的composer.json文件的auto-load.classmap部分,並在命令行上運行composer dumpautoload;composer install。 此外,這是使用自定義的輔助函數:

    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; 
        } 
    } 
    
  • 添加一些測試,只有端點的路線。 (可悲的是,將 $this->app->make(Router::class)->match($method, $endpoint, $closure); 您的測試中是行不通的,只要我可以告訴)。 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); }); ,如果你願意,你甚至可以把這個包在if塊,即確保config('app.debug') == true第一。

  • 配置響應的內容以反映提示特定response值的端點。在你的測試中放置這樣的東西。 app(ResponseFactory::class)->setResponse([[ 'url.path' => "/curltest/$curlTargetEndpont", 'response' => 'success' ]]);

  • 相關問題