gpt4 book ai didi

Symfony 4 模拟私有(private)服务

转载 作者:行者123 更新时间:2023-12-02 00:57:42 28 4
gpt4 key购买 nike

我有负责获取各种 api 来收集数据的应用程序。我使用 Codeception 作为我的测试框架,我需要在我的功能测试中模拟 API 客户端类,如下所示:

public function testFetchingNewApps(FunctionalTester $I) {
$request = new Request(
SymfonyRequest::METHOD_GET,
'https://url.com/get'
);

$apiClientMock = \Mockery::mock(HttpClientInterface::class);
$apiClientMock
->shouldReceive('send')
->with($request)
->andReturn(new Response(HttpCode::OK, [], '{"data":"some data"}'))
->once();

$symfony = $this->getModule('Symfony')->grabService('kernel')->getContainer()->set(HttpClientInterface::class, $apiClientMock);
$symfony->persistService(HttpClientInterface::class, false);

$I->runShellCommand('bin/console sync:apos --env=test');
}

但是自从 Symfony 4 以来,我们无法访问私有(private)服务来模拟它们,我看到了类似的错误

service is private, you cannot replace it.



于是发现可以创建 ApiClinetMock.php whick 扩展实 ApiCLient.php文件和 services_test.yml文件。在 services_test.yml我可以制作 ApiClinetMock.php作为公共(public)服务并将其与接口(interface)关联(覆盖接口(interface)使用):
#services_test.yml
services:
_defaults:
public: true
Api\Tests\functional\Mock\ApiClientMock: ~
ApiHttpClients\HttpClientInterface: '@Api\Tests\functional\Mock\ApiClientMock'

现在,当我运行测试用例时,我看不到任何错误,例如

service is private, you cannot replace it.



但是我的模拟不起作用并返回真实数据而不是我在模拟中设置的数据,我不知道为什么。

可能的解决方案是覆盖我在 ApiClientMock 中需要的方法以返回我需要的数据,但它仅适用于一个测试用例,但我需要测试各种不同的有效/无效响应。

我知道 Symfony 4 中有很多关于这个问题的信息,但我仍然找不到任何好的例子。有人可以向我解释我应该如何编写功能测试以及如何制作适当的模拟。

更新 我知道我可以使用 https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing但它仅在您需要获得私有(private)服务时有效,但在您需要设置/替换时无效

更新 我也尝试设置 Api\Tests\functional\Mock\ApiClientMock作为合成但现在我收到错误:

The "Api\Tests\functional\Mock\ApiClientMock" service is synthetic, it needs to be set at boot time before it can be used.

最佳答案

好的,我找到了为什么我仍然获得真实数据而不是 mock 的原因。问题是 Codeception 使用 CLI 模块 ( https://codeception.com/docs/modules/Cli ) 正在运行新应用程序,因此数据不会在那里模拟。为了解决这个问题,我扩展了 Symfony 模块以使用 Symfony CommandTester ( https://symfony.com/doc/current/console.html#testing-commands ) 而不是 Codeception CLI 模块。
例如我有 HttpClientInterface:

<?php declare(strict_types = 1);

namespace App\Infrastructure\HttpClients;

use App\Infrastructure\HttpClients\Exceptions\HttpClientException;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* Interface HttpClientInterface
* @package OfferManagement\Infrastructure\ApiOfferSync\HttpClients
*/
interface HttpClientInterface
{
/**
* Send an HTTP request.
*
* @param RequestInterface $request Request to send
* @param array|array[]|string[]|integer[] $options Request options to apply to the given
* request and to the transfer.
*
* @return ResponseInterface
* @throws HttpClientException
*/
public function send(RequestInterface $request, array $options = []): ResponseInterface;

/**
* Asynchronously send an HTTP request.
*
* @param RequestInterface $request Request to send
* @param array|array[]|string[]|integer[] $options Request options to apply to the given
* request and to the transfer.
*
* @return PromiseInterface
*/
public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface;
}
和他的实现 GuzzleApiClient:
<?php declare(strict_types = 1);

namespace App\Infrastructure\HttpClients\Adapters\Guzzle;

use App\Infrastructure\HttpClients\Exceptions\HttpClientException;
use App\Infrastructure\HttpClients\HttpClientInterface;
use GuzzleHttp\Client;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

class GuzzleApiClient implements HttpClientInterface
{
/**
* @var Client
*/
private $apiClient;

/**
* GuzzleApiClient constructor.
*/
public function __construct()
{
$this->apiClient = new Client();
}

/**
* @param RequestInterface $request Request to send
* @param array|array[]|string[]|integer[] $options Request options to apply to the given
* request and to the transfer.
*
* @return ResponseInterface
* @throws HttpClientException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function send(RequestInterface $request, array $options = []):ResponseInterface
{
try {
return $this->apiClient->send($request, $options);
} catch (\Throwable $e) {
throw new HttpClientException($e->getMessage());
}
}

/**
* Asynchronously send an HTTP request.
*
* @param RequestInterface $request Request to send
* @param array|array[]|string[]|integer[] $options Request options to apply to the given
* request and to the transfer.
*
* @return PromiseInterface
* @throws HttpClientException
*/
public function sendAsync(RequestInterface $request, array $options = []):PromiseInterface
{
try {
return $this->apiClient->sendAsync($request, $options);
} catch (\Throwable $e) {
throw new HttpClientException($e->getMessage());
}
}
}
原创 service.yml我所有标记为私有(private)的服务:
        services:
_defaults:
autowire: true
autoconfigure: true
public: false


App\Infrastructure\HttpClients\Adapters\Guzzle\GuzzleApiClient:
shared: false
所以我无法在测试中访问它们来模拟,我需要创建 service_test.yml并将所有服务设置为公共(public),我需要创建应该实现 HttpClientInterface 的 stub 类还可以模拟请求并将其与 HttpClientInterface 相关联在 services_test.yml .
services_test.yml
services:
_defaults:
public: true

### to mock HttpClientInterface we need to override implementation for test env, note original implementation is not shared but here it should be shared
### as we need to always get same instance, but in the GuzzleApiClient we need add logic to clear data somehow after each test
App\Tests\functional\Mock\GuzzleApiClient: ~
App\Infrastructure\HttpClients\HttpClientInterface: '@App\Tests\functional\Mock\GuzzleApiClient'
应用\测试\功能\模拟\GuzzleApiClient:
<?php declare(strict_types=1);

namespace OfferManagement\Tests\functional\ApiOfferSync\Mock;

use App\Infrastructure\HttpClients
use App\Infrastructure\HttpClients\Adapters\Guzzle\Request;
use GuzzleHttp\Psr7\Response;
use App\Infrastructure\HttpClients\Exceptions\HttpClientException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

/**
* Class we using as a mock for HttpClientInterface. NOTE: this class is shared so we need clean up mechanism to remove
* prepared data after usage to avoid unexpected situations
* @package App\Tests\functional\Mock
*/
class GuzzleApiClient implements HttpClientInterface
{
/**
* @var array
*/
private $responses;

/**
* @param RequestInterface $request
* @param array $options
* @return ResponseInterface
* @throws HttpClientException
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function send(RequestInterface $request, array $options = []): ResponseInterface
{
$url = urldecode($request->getUri()->__toString());
$url = md5($url);
if(isset($this->responses[$url])) {
$response = $this->responses[$url];
unset($this->responses[$url]);

return $response;
}

throw \Exception('No mocked response for such request')

}


/**
* Url is to long to be array key, so I'm doing md5 to make it shorter
* @param RequestInterface $request
* @param Response $response
*/
public function addResponse(RequestInterface $request, Response $response):void
{
$url = urldecode($request->getUri()->__toString());
$url = md5($url);
$this->responses[$url] = $response;
}

}
在这一点上,我们有机制来模拟请求,如下所示:
$apiClient = $I->grabService(HttpCLientInterface::class);
$apiClient->addResponse($response);
$I->_getContainer()->set(HttpClientInterface::class, $apiClient)
但它不适用于 CLI,因为我们需要实现 CommandTester正如我在开头提到的那样。为此,我需要扩展 Codeception Symfony 模块:
<?php declare(strict_types=1);

namespace App\Tests\Helper;


use Codeception\Exception\ModuleException;
use Codeception\TestInterface;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\DependencyInjection\ContainerInterface;


class SymfonyExtended extends \Codeception\Module\Symfony
{
private $commandOutput = '';

public $output = '';

public function _before(TestInterface $test)
{
parent::_before($test);
$this->commandOutput = '';
}

public function _initialize()
{
parent::_initialize();
}

/**
* @param string $commandName
* @param array $arguments
* @param array $options
* @throws ModuleException
*/
public function runCommand(string $commandName, array $arguments = [], array $options = [])
{
$application = new Application($this->kernel);
$command = $application->find($commandName);
$commandTester = new CommandTester($command);

$commandTester->execute(
$this->buildCommandArgumentsArray($command, $arguments, $options)
);

$this->commandOutput = $commandTester->getDisplay();
if ($commandTester->getStatusCode() !== 0 && $commandTester->getStatusCode() !== null) {
\PHPUnit\Framework\Assert::fail("Result code was {$commandTester->getStatusCode()}.\n\n");
}
}

/**
* @param Command $command
* @param array $arguments
* @param array $options
* @throws ModuleException
* @return array
*/
private function buildCommandArgumentsArray(Command $command, array $arguments, array $options):array
{
$argumentsArray['command'] = $command->getName();
if(!empty($arguments)) {
foreach ($arguments as $name => $value) {
$this->validateArgument($name, $value);
$argumentsArray[$name] = $value;
}
}

if(!empty($options)) {
foreach ($options as $name => $value) {
$this->validateArgument($name, $value);
$argumentsArray['--'.$name] = $value;
}
}

return $argumentsArray;
}

/**
* @param $key
* @param $value
* @throws ModuleException
*/
private function validateArgument($key, $value)
{

if(
!is_string($key)
|| empty($value)
) {
throw new ModuleException('each argument provided to symfony command should be in format: "argument_name" => "value". Like: "username" => "Wouter"');
}

if($key === 'command') {
throw new ModuleException('you cant add arguments or options with name "command" to symofny commands');
}
}

}
就是这样!现在我们可以模拟 HttpCLientInterface 并运行 $I->runCommand('app:command') :
$apiClient = $I->grabService(HttpCLientInterface::class);
$apiClient->addResponse($response);
$I->_getContainer()->set(HttpClientInterface::class, $apiClient);
$I->runCommand('app:command');
这是简化版,我可能会遗漏一些东西,请随时询问您是否需要一些解释!

关于Symfony 4 模拟私有(private)服务,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53160862/

28 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com