需要在 Laravel 5.1 中测试使用 CURL 的服务
Need to Test a Service that Uses CURL in Laravel 5.1
我为我的 Laravel 5.1 API 构建了一个搜索 YouTube 的服务。我正在尝试为其编写测试,但无法弄清楚如何模拟该功能。下面是服务。
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 来测试控制器。现在我需要弄清楚如何测试上面的服务。感谢任何帮助。
需要说明的是,由于硬编码的 curl_*
方法,您的 class 不是为孤立的单元测试而设计的。为了让它变得更好,你至少有两个选择:
1) 将 curl_*
函数调用提取到另一个 class 并将该 class 作为参数传递
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 class。
有很多现成的解决方案可以抽象网络。例如,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;
}
}
运行 测试和...我的天啊,失败了!!1 你在 extractVideo
方法中有拼写错误,应该是 $item
而不是 $items
。让我们修复它
$videoId = $item['id']['videoId'];
$title = $item['snippet']['title'];
$description = $item['snippet']['description'];
$thumbnail = $item['snippet']['thumbnails']['default']['url'];
好的,现在它通过了。
如果你想通过调用 Youtube API 来测试你的 class 你只需要创建普通的 Youtube class.
顺便说一句,有 php-youtube-api 库,它有 laravel 4 和 laravel 5 的提供者,也有测试
如果更改进行 CURL 调用的代码不是一个选项,它仍然可以完成,但它并不漂亮。
此解决方案假定进行 CURL 调用的代码将其目标 URL 基于环境变量。这里的要点是您可以将调用重定向回您自己的应用程序,重定向到输出可以由您的测试控制的端点。由于执行测试的应用程序实例实际上与 CURL 调用掉头时访问的实例不同,我们处理范围问题的方式允许测试控制输出通过 forever
缓存,将您的虚拟数据记录到外部文件中,该文件在 运行 时间访问。
- 在测试中,改变负责 CURL 调用域的环境变量的值,使用:
putenv("SOME_BASE_URI=".config('app.url')."/curltest/")
由于 phpunit.xml
通常将默认 CACHE_DRIVER
设置为 array
,这是 而非 永久性的,您必须将其放入您的测试将其改回 file
.
config(['cache.default' => 'file']);
在您的 tests
文件夹中创建一个新的 class,当请求满足一组可配置的条件时,它将负责返回给定的响应:
使用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);
}
}
因为它在 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'
]]);
我为我的 Laravel 5.1 API 构建了一个搜索 YouTube 的服务。我正在尝试为其编写测试,但无法弄清楚如何模拟该功能。下面是服务。
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 来测试控制器。现在我需要弄清楚如何测试上面的服务。感谢任何帮助。
需要说明的是,由于硬编码的 curl_*
方法,您的 class 不是为孤立的单元测试而设计的。为了让它变得更好,你至少有两个选择:
1) 将 curl_*
函数调用提取到另一个 class 并将该 class 作为参数传递
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 class。 有很多现成的解决方案可以抽象网络。例如,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;
}
}
运行 测试和...我的天啊,失败了!!1 你在 extractVideo
方法中有拼写错误,应该是 $item
而不是 $items
。让我们修复它
$videoId = $item['id']['videoId'];
$title = $item['snippet']['title'];
$description = $item['snippet']['description'];
$thumbnail = $item['snippet']['thumbnails']['default']['url'];
好的,现在它通过了。
如果你想通过调用 Youtube API 来测试你的 class 你只需要创建普通的 Youtube class.
顺便说一句,有 php-youtube-api 库,它有 laravel 4 和 laravel 5 的提供者,也有测试
如果更改进行 CURL 调用的代码不是一个选项,它仍然可以完成,但它并不漂亮。
此解决方案假定进行 CURL 调用的代码将其目标 URL 基于环境变量。这里的要点是您可以将调用重定向回您自己的应用程序,重定向到输出可以由您的测试控制的端点。由于执行测试的应用程序实例实际上与 CURL 调用掉头时访问的实例不同,我们处理范围问题的方式允许测试控制输出通过 forever
缓存,将您的虚拟数据记录到外部文件中,该文件在 运行 时间访问。
- 在测试中,改变负责 CURL 调用域的环境变量的值,使用:
putenv("SOME_BASE_URI=".config('app.url')."/curltest/")
由于 phpunit.xml
通常将默认 CACHE_DRIVER
设置为 array
,这是 而非 永久性的,您必须将其放入您的测试将其改回 file
.
config(['cache.default' => 'file']);
在您的
tests
文件夹中创建一个新的 class,当请求满足一组可配置的条件时,它将负责返回给定的响应:使用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); }
}
因为它在 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' ]]);