2015-12-10 6 views
5

Zbudowałem usługę dla mojego interfejsu API Laravel 5.1, który przeszukuje YouTube. Próbuję napisać test na to, ale mam problem z wymyśleniem, jak kpić z funkcjonalności. Poniżej znajduje się usługa.Konieczność przetestowania usługi wykorzystującej CURL w Laravel 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]; 
} 
} 

Utworzyłem tę usługę, aby wyodrębnić funkcjonalność z kontrolera. Następnie użyłem Mockery, aby przetestować kontroler. Teraz muszę dowiedzieć się, jak przetestować powyższą usługę. Każda pomoc jest doceniana.

Odpowiedz

3

Trzeba powiedzieć, że Twoja klasa nie została zaprojektowana do testowania pojedynczych jednostek z powodu zakodowanych na stałe metod: curl_*. Na zrobić to lepiej mieć co najmniej 2 opcje:

1) ekstraktu curl_* funkcje wzywa do innej klasy i przekazać tę klasę jako parametr

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

Teraz można łatwo mock klasy CurlCaller. Istnieje wiele gotowych rozwiązań, które umożliwiają rozwój sieci. Na przykład: Guzzle jest świetny

2) Inną opcją jest wyodrębnienie wywołań curl_* do metody chronionej i wyśmiewanie tej metody. Oto przykład roboczych:

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

Teraz można drwić metodę callUrl. Ale najpierw przeprowadź oczekiwaną odpowiedź API na plik 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; 
    } 
} 

Test Run i ... OMG BŁĄD !! 1 Musisz literówki w extractVideo metody powinny być $item zamiast $items. Pozwala to naprawić

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

OK, teraz przechodzi.


Jeśli chcesz przetestować swoją klasę, korzystając z interfejsu API YouTube, musisz utworzyć normalną klasę Youtube.


BTW, nie ma php-youtube-api lib, który ma dostawców dla laravel 4 i laravel 5, również ma testy

+0

Dziękuję bardzo! – WebDev84

0

przypadku zmiany kodu, gdzie wykonywane są połączenia CURL nie jest opcją, może wciąż jest zrobione, ale nie jest ładne.

To rozwiązanie zakłada, że ​​kod wywołujący CURL opiera swój docelowy adres URL na zmiennej środowiskowej. Istotą jest to, że możesz przekierować połączenie z powrotem do własnej aplikacji, do punktu końcowego, gdzie wyjście może być kontrolowane przez twój test. Ponieważ instancja aplikacji, w której test jest wykonywany, różni się faktycznie od tego, do którego uzyskuje się dostęp, gdy wywołanie CURL wykonuje obrót, sposób, w jaki radzimy sobie z problemami z określaniem zakresu, aby umożliwić testowi sterowanie wyjściem, poprzez pamięć podręczną o wartości forever, który zapisuje twoje dane-dummy w zewnętrznym pliku, do którego uzyskuje się dostęp w czasie wykonywania.

  1. W teście zmieniać wartość zmiennej środowiskowej odpowiedzialną za domenie zaproszenia CURL używając: putenv("SOME_BASE_URI=".config('app.url')."/curltest/")

Od phpunit.xml zazwyczaj ustawia domyślne CACHE_DRIVER do array, który jest nie na stałe, musisz umieścić to w teście, aby zmienić go z powrotem na file.

config(['cache.default' => 'file']); 
  1. Utwórz nową klasę w folderze tests, który będzie odpowiedzialny za zwrot danej odpowiedzi, gdy wniosek spełnia konfigurowalny zestaw kryteriów:

    wykorzystania oświetlania \ Http \ Request;

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

    }

Ponieważ jest to w folderze tests a nie pod nazw App, należy dodać go do części auto-load.classmap swojego pliku composer.json, i uruchom composer dumpautoload;composer install w linii poleceń. Poza tym jest za pomocą funkcji niestandardowych pomocnika:

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. Dodaj kilka punktów końcowych badania, tylko do swoich tras. (Niestety, umieszczenie $this->app->make(Router::class)->match($method, $endpoint, $closure); w swoim teście nie będzie działać, o ile mogę powiedzieć.) 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); }); Można nawet owinąć to w bloku if jeśli chcesz, że pilnuje config('app.debug') == true pierwszy.

  2. Skonfiguruj zawartość odpowiedzi tak, aby odzwierciedlała punkt końcowy, który ma sugerować określoną wartość response. Umieść coś takiego w teście. app(ResponseFactory::class)->setResponse([[ 'url.path' => "/curltest/$curlTargetEndpont", 'response' => 'success' ]]);