对 Symfony 服务进行单元测试 Class
Unit testing a Symfony Service Class
我正在寻找有关如何为 Symfony 服务编写单元测试的指导 class。一整天都在网上搜索,但我发现的最多的是关于旧 phpunit 版本和旧 Symfony 版本的过时问题和答案。
我是 运行 Symfony 4 并且有一个名为 ApiService.php 的服务 class。此 class 连接到外部 API 服务,我不是要测试此外部 API 服务,而是使用固定数据集测试我自己的方法。
class 的一个非常精简的版本是这样的,位于文件夹 src/Service/ApiService.php:
<?php
namespace App\Service;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Uri;
use JsonException;
class ApiService
{
/**
* Set if test environment is enabled
*
* @var bool
* @since 1.0.0
*/
private bool $test;
/**
* User key for API authentication
*
* @var string
* @since 1.0.0
*/
private string $userKey;
/**
* Construct the class.
*
* @param bool $test Set API mode
* @param string $key Set the API token
*
* @since 1.0.0
*/
public function __construct(bool $test, string $key)
{
$this->userKey = $key;
$this->test = $test;
}
/**
* Search companies.
*
* @param array $params Parameters to filter the query on
* @param array $companies List of retrieved companies
*
* @return array List of companies.
*
* @since 1.0.0
* @throws JsonException
* @throws GuzzleException
*/
public function getCompanies(array $params, array $companies = []): array
{
$results = $this->callApi('search/companies', $params);
if (isset($results['data']['items'])) {
$companies = array_merge(
$companies,
$results['data']['items']
);
}
$nextLink = $results['data']['nextLink'] ?? null;
if ($nextLink) {
$uri = new Uri($nextLink);
parse_str($uri->getQuery(), $params);
$companies = $this->getCompanies($params, $companies);
}
return $companies;
}
/**
* Call the API.
*
* @param string $destination The endpoint to call
* @param array $params The parameters to pass to the API
*
* @return array API details.
*
* @since 1.0.0
* @throws GuzzleException|JsonException
*/
private function callApi(string $destination, array $params = []): array
{
$client = new Client(['base_uri' => 'https://test.com/']);
if ($this->test) {
$destination = 'test' . $destination;
}
if ($this->userKey) {
$params['user_key'] = $this->userKey;
}
$response = $client->get($destination, ['query' => $params]);
return json_decode(
$response->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR
);
}
}
这是测试 class 到目前为止我已经完成但它不起作用:
<?php
namespace App\Tests\Service;
use App\Service\ApiService;
use PHPUnit\Framework\TestCase;
class ApiServiceTest extends TestCase
{
public function testGetCompanies()
{
$result = ['data' => [
'items' => [
1 => 'first',
2 => 'second'
]
];
$apiService = $this->getMockBuilder(ApiService::class)
->disableOriginalConstructor()
->getMock();
$apiService->method('callApi')
->with($result);
$result = $apiService->getCompanies([]);
print_r($result);
}
}
我不明白的是一些事情。
首先 class 我应该扩展:
- 测试用例
- Web测试用例
- 内核测试用例
其次,我如何设置模拟数据,这样我就不会去外部 API 而是传入我定义的 $result。
如前所述,我不打算测试外部 API,而是希望我的方法在给定要测试的示例数据的情况下始终按照测试中的设计运行。
任何提示将不胜感激。
您应该从 PHPUnit 的 TestCase
扩展。如果您想进行功能测试,WebTestCase
和 KernelTestCase
很有用。您的案例是 classic 单元测试:您想单独测试 ApiService
。
ApiService
目前实际上在做两件事:
- 打电话
- 正在处理数据
通过引入专用的 API 客户端将它们彼此分开是个好主意:
interface ApiClient
{
public function call(string $destination, array $params = []): array;
}
对于您的生产代码,您可以使用 Guzzle 创建一个实现。您可以为发出实际 http 请求的 GuzzleApiClient
编写集成测试,以确保它以预期的方式处理响应。
您的 ApiService
现在归结为:
final class ApiService
{
private ApiClient $apiClient;
public function __construct(ApiClient $apiClient)
{
$this->apiClient = $apiClient;
}
public function getCompanies(array $params, array $companies = []): array
{
$results = $this->apiClient->call('search/companies', $params);
if (isset($results['data']['items'])) {
$companies = array_merge(
$companies,
$results['data']['items']
);
}
$nextLink = $results['data']['nextLink'] ?? null;
if ($nextLink) {
parse_str(parse_url($nextLink, PHP_URL_QUERY), $params);
$companies = $this->getCompanies($params, $companies);
}
return $companies;
}
}
因为我不知道 ApiService
到底做了什么,所以我编写了这些示例测试:
/**
* @covers \App\Service\ApiService
*/
class ApiServiceTest extends TestCase
{
/**
* @var MockObject&ApiClient
*/
private ApiClient $apiClient;
private ApiService $subject;
public function testGetCompanies()
{
$this->apiClient->addResponse(
'search/companies',
[],
['data' => ['items' => [1 => 'first', 2 => 'second']]]
);
$result = $this->subject->getCompanies([]);
self::assertEquals(['first', 'second'], $result);
}
public function testGetCompaniesPaginated()
{
$this->apiClient->addResponse(
'search/companies',
[],
['data' => ['items' => [1 => 'first', 2 => 'second'], 'nextLink' => 'search/companies?page=2']]
);
$this->apiClient->addResponse(
'search/companies',
['page' => 2],
['data' => ['items' => [1 => 'third', 2 => 'fourth'], 'nextLink' => 'search/companies?page=3']]
);
$this->apiClient->addResponse(
'search/companies',
['page' => 3],
['data' => ['items' => [1 => 'fifth']]]
);
$result = $this->subject->getCompanies([]);
self::assertEquals(['first', 'second', 'third', 'fourth', 'fifth'], $result);
}
protected function setUp(): void
{
parent::setUp();
$this->apiClient = new class implements ApiClient {
private array $responses = [];
public function call(string $destination, array $params = []): array
{
return $this->responses[$this->key($destination, $params)] ?? [];
}
public function addResponse(string $destination, array $params, array $response)
{
$this->responses[$this->key($destination, $params)] = $response;
}
private function key(string $destination, array $params): string
{
return $destination . implode('-', $params);
}
};
$this->subject = new ApiService($this->apiClient);
}
}
我为 ApiClient
实现创建了一个匿名 class。这只是一个例子。当然,您也可以使用 PHPUnit 的模拟、Prophecy 或您喜欢的任何模拟框架。但我发现创建专门的测试替身通常更容易。
在我的项目中,它有助于将 HttpClient 注入服务 ,例如在构造函数中使用 HttpClientInterface $httpClient
。之后,您在该服务中有一个可交换的客户端并停止在您的服务中创建它。
一个非常简单的测试用例可以如下所示。它检查 API 请求是否完成,仅此而已:
public function testRequestIsExecuted(): void
{
$callbackWasCalled = false;
$callback = function ($method, $url, $options) use (&$callbackWasCalled) {
$callbackWasCalled = true;
return new MockResponse('[]');
};
$mockedClient = new MockHttpClient($callback);
$apiService = new Apiservice($mockedClient);
$result = $apiService->getCompanies([]);
$this->assertTrue($callbackWasCalled);
}
您想做更详细的检查吗?没问题,只需 fiddle 与您的回调参数相关:您可以比较方法类型 (GET/POST),return 基于调用的 URL 的不同响应, ....
我正在寻找有关如何为 Symfony 服务编写单元测试的指导 class。一整天都在网上搜索,但我发现的最多的是关于旧 phpunit 版本和旧 Symfony 版本的过时问题和答案。
我是 运行 Symfony 4 并且有一个名为 ApiService.php 的服务 class。此 class 连接到外部 API 服务,我不是要测试此外部 API 服务,而是使用固定数据集测试我自己的方法。
class 的一个非常精简的版本是这样的,位于文件夹 src/Service/ApiService.php:
<?php
namespace App\Service;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Uri;
use JsonException;
class ApiService
{
/**
* Set if test environment is enabled
*
* @var bool
* @since 1.0.0
*/
private bool $test;
/**
* User key for API authentication
*
* @var string
* @since 1.0.0
*/
private string $userKey;
/**
* Construct the class.
*
* @param bool $test Set API mode
* @param string $key Set the API token
*
* @since 1.0.0
*/
public function __construct(bool $test, string $key)
{
$this->userKey = $key;
$this->test = $test;
}
/**
* Search companies.
*
* @param array $params Parameters to filter the query on
* @param array $companies List of retrieved companies
*
* @return array List of companies.
*
* @since 1.0.0
* @throws JsonException
* @throws GuzzleException
*/
public function getCompanies(array $params, array $companies = []): array
{
$results = $this->callApi('search/companies', $params);
if (isset($results['data']['items'])) {
$companies = array_merge(
$companies,
$results['data']['items']
);
}
$nextLink = $results['data']['nextLink'] ?? null;
if ($nextLink) {
$uri = new Uri($nextLink);
parse_str($uri->getQuery(), $params);
$companies = $this->getCompanies($params, $companies);
}
return $companies;
}
/**
* Call the API.
*
* @param string $destination The endpoint to call
* @param array $params The parameters to pass to the API
*
* @return array API details.
*
* @since 1.0.0
* @throws GuzzleException|JsonException
*/
private function callApi(string $destination, array $params = []): array
{
$client = new Client(['base_uri' => 'https://test.com/']);
if ($this->test) {
$destination = 'test' . $destination;
}
if ($this->userKey) {
$params['user_key'] = $this->userKey;
}
$response = $client->get($destination, ['query' => $params]);
return json_decode(
$response->getBody()->getContents(),
true,
512,
JSON_THROW_ON_ERROR
);
}
}
这是测试 class 到目前为止我已经完成但它不起作用:
<?php
namespace App\Tests\Service;
use App\Service\ApiService;
use PHPUnit\Framework\TestCase;
class ApiServiceTest extends TestCase
{
public function testGetCompanies()
{
$result = ['data' => [
'items' => [
1 => 'first',
2 => 'second'
]
];
$apiService = $this->getMockBuilder(ApiService::class)
->disableOriginalConstructor()
->getMock();
$apiService->method('callApi')
->with($result);
$result = $apiService->getCompanies([]);
print_r($result);
}
}
我不明白的是一些事情。
首先 class 我应该扩展:
- 测试用例
- Web测试用例
- 内核测试用例
其次,我如何设置模拟数据,这样我就不会去外部 API 而是传入我定义的 $result。
如前所述,我不打算测试外部 API,而是希望我的方法在给定要测试的示例数据的情况下始终按照测试中的设计运行。
任何提示将不胜感激。
您应该从 PHPUnit 的 TestCase
扩展。如果您想进行功能测试,WebTestCase
和 KernelTestCase
很有用。您的案例是 classic 单元测试:您想单独测试 ApiService
。
ApiService
目前实际上在做两件事:
- 打电话
- 正在处理数据
通过引入专用的 API 客户端将它们彼此分开是个好主意:
interface ApiClient
{
public function call(string $destination, array $params = []): array;
}
对于您的生产代码,您可以使用 Guzzle 创建一个实现。您可以为发出实际 http 请求的 GuzzleApiClient
编写集成测试,以确保它以预期的方式处理响应。
您的 ApiService
现在归结为:
final class ApiService
{
private ApiClient $apiClient;
public function __construct(ApiClient $apiClient)
{
$this->apiClient = $apiClient;
}
public function getCompanies(array $params, array $companies = []): array
{
$results = $this->apiClient->call('search/companies', $params);
if (isset($results['data']['items'])) {
$companies = array_merge(
$companies,
$results['data']['items']
);
}
$nextLink = $results['data']['nextLink'] ?? null;
if ($nextLink) {
parse_str(parse_url($nextLink, PHP_URL_QUERY), $params);
$companies = $this->getCompanies($params, $companies);
}
return $companies;
}
}
因为我不知道 ApiService
到底做了什么,所以我编写了这些示例测试:
/**
* @covers \App\Service\ApiService
*/
class ApiServiceTest extends TestCase
{
/**
* @var MockObject&ApiClient
*/
private ApiClient $apiClient;
private ApiService $subject;
public function testGetCompanies()
{
$this->apiClient->addResponse(
'search/companies',
[],
['data' => ['items' => [1 => 'first', 2 => 'second']]]
);
$result = $this->subject->getCompanies([]);
self::assertEquals(['first', 'second'], $result);
}
public function testGetCompaniesPaginated()
{
$this->apiClient->addResponse(
'search/companies',
[],
['data' => ['items' => [1 => 'first', 2 => 'second'], 'nextLink' => 'search/companies?page=2']]
);
$this->apiClient->addResponse(
'search/companies',
['page' => 2],
['data' => ['items' => [1 => 'third', 2 => 'fourth'], 'nextLink' => 'search/companies?page=3']]
);
$this->apiClient->addResponse(
'search/companies',
['page' => 3],
['data' => ['items' => [1 => 'fifth']]]
);
$result = $this->subject->getCompanies([]);
self::assertEquals(['first', 'second', 'third', 'fourth', 'fifth'], $result);
}
protected function setUp(): void
{
parent::setUp();
$this->apiClient = new class implements ApiClient {
private array $responses = [];
public function call(string $destination, array $params = []): array
{
return $this->responses[$this->key($destination, $params)] ?? [];
}
public function addResponse(string $destination, array $params, array $response)
{
$this->responses[$this->key($destination, $params)] = $response;
}
private function key(string $destination, array $params): string
{
return $destination . implode('-', $params);
}
};
$this->subject = new ApiService($this->apiClient);
}
}
我为 ApiClient
实现创建了一个匿名 class。这只是一个例子。当然,您也可以使用 PHPUnit 的模拟、Prophecy 或您喜欢的任何模拟框架。但我发现创建专门的测试替身通常更容易。
在我的项目中,它有助于将 HttpClient 注入服务 ,例如在构造函数中使用 HttpClientInterface $httpClient
。之后,您在该服务中有一个可交换的客户端并停止在您的服务中创建它。
一个非常简单的测试用例可以如下所示。它检查 API 请求是否完成,仅此而已:
public function testRequestIsExecuted(): void
{
$callbackWasCalled = false;
$callback = function ($method, $url, $options) use (&$callbackWasCalled) {
$callbackWasCalled = true;
return new MockResponse('[]');
};
$mockedClient = new MockHttpClient($callback);
$apiService = new Apiservice($mockedClient);
$result = $apiService->getCompanies([]);
$this->assertTrue($callbackWasCalled);
}
您想做更详细的检查吗?没问题,只需 fiddle 与您的回调参数相关:您可以比较方法类型 (GET/POST),return 基于调用的 URL 的不同响应, ....