2015-12-10 7 views
5

Ich baute einen Service für meine Laravel 5.1 API, die YouTube sucht. Ich versuche, einen Test dafür zu schreiben, habe aber Probleme herauszufinden, wie man die Funktionalität verspotten kann. Unten ist der Service.Müssen einen Dienst testen, der CURL in Laravel verwendet 5.1

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

Ich habe diesen Dienst erstellt, um die Funktionalität von einem Controller zu abstrahieren. Ich benutzte dann Mockery, um den Controller zu testen. Jetzt muss ich herausfinden, wie man den oben genannten Dienst testet. Jede Hilfe wird geschätzt.

Antwort

3

Müssen Sie sagen, Ihre Klasse ist nicht für isolierte Einheit Tests wegen hardcoded curl_* Methoden konzipiert. Für machen es besser, haben Sie mindestens 2 Möglichkeiten:

1) Extrahieren curl_* Funktionen aufruft, um eine andere Klasse und übergeben Sie diese Klasse als 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; 
    } 
} 

Jetzt können Sie leicht Mock CurlCaller Klasse. Es gibt viele fertige Lösungen, die das Netzwerk abstrahieren. Zum Beispiel Guzzle ist groß

2) Eine andere Option ist, curl_* Aufrufe an die geschützte Methode zu extrahieren und diese Methode zu verspotten. Hier ist ein funktionierendes Beispiel:

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

Jetzt können Sie verspotten Methode callUrl. Aber zuerst, lassen Sie die erwartete API-Antwort auf fixtures/youtube-response-stub.json Datei setzen.

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 und ... OMG AUSFALL !! 1 Sie haben Fehler in extractVideo Methode sollte $item statt $items sein. Lets fix it

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

OK, jetzt es übergeben.

Wenn Sie Ihre Klasse mit Anruf auf Youtube API testen möchten, müssen Sie nur normale Youtube-Klasse erstellen.


BTW, gibt es php-youtube-api lib, die Anbieter für Laravel 4 und Laravel hat 5, hat es auch Tests

+0

Vielen Dank! – WebDev84

0

Wenn Sie den Code zu ändern, wo die CURL Anrufe sind nicht möglich ist, kann es immer noch gemacht, aber es ist nicht schön.

Bei dieser Lösung wird davon ausgegangen, dass der Code, der den CURL-Aufruf ausführt, seine Ziel-URL auf einer Umgebungsvariablen basiert. Der Kernpunkt hier ist, dass Sie den Anruf zurück zu Ihrer eigenen App zu einem Endpunkt umleiten können, wo die Ausgabe von Ihrem Test gesteuert werden kann. Da die Instanz der App, in der der Test ausgeführt wird, sich von der unterscheidet, auf die beim Aufruf des CURL-Aufrufs zugegriffen wird, ist die Art und Weise, wie wir mit Problemen umgehen, damit der Test die Ausgabe steuern kann, durch forever Cache. Diese zeichnet Ihre Dummy-Daten in einer externen Datei auf, auf die zur Laufzeit zugegriffen wird.

  1. Im Test, den Wert der Umgebungsvariablen, die für die Domäne des mit CURL Aufruf ändern: putenv("SOME_BASE_URI=".config('app.url')."/curltest/")

Da phpunit.xml typischerweise die CACHE_DRIVER-array Standardsätze, die nicht dauerhaft, müssen Sie dies in Ihrem Test setzen, um es zurück zu file zu ändern.

config(['cache.default' => 'file']); 
  1. Erstellen Sie eine neue Klasse in Ihrem tests Ordner, der für die Rückgabe einer gegebenen Reaktion verantwortlich sein wird, wenn die Anforderung eine konfigurierbare Menge von Kriterien erfüllt:

    Verwendung Illuminate \ Http \ Anfrage;

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

    }

Da dies im tests Ordner und nicht unter dem App Namespace, sollten Sie es auf den auto-load.classmap Teil Ihrer composer.json Datei hinzuzufügen, und führen Sie composer dumpautoload;composer install in der Befehlszeile aus. Auch dies wird mit einer benutzerdefinierten Hilfsfunktion:

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. einige hinzufügen Test-only-Endpunkte auf Ihre Routen. (Leider in Ihrem Test $this->app->make(Router::class)->match($method, $endpoint, $closure); Platzierung wird nicht funktionieren, soweit ich das sagen kann.) 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); }); Sie können sogar diese wickeln in einem if Block, wenn Sie wollen, das macht sicher config('app.debug') == true zuerst.

  2. Konfigurieren Sie den Inhalt der Antworten so, dass er den Endpunkt widerspiegelt, der einen spezifischen response-Wert ausgeben soll. Legen Sie so etwas in Ihren Test.