代码之家  ›  专栏  ›  技术社区  ›  Bogdan Dubyk

Symfony 4模拟私人服务

  •  3
  • Bogdan Dubyk  · 技术社区  · 7 年前

    我有一个应用程序,负责获取各种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');
    }
    

    但是由于symfony4我们无法访问私有服务来模拟它们,我看到了这样的错误

    所以我发现我可以创造 ApiClinetMock.php ApiCLient.php 文件和 services_test.yml 服务测试.yml 我能做的 ApiClinetMock.php文件

    #services_test.yml
    services:
        _defaults:
            public: true
        Api\Tests\functional\Mock\ApiClientMock: ~
        ApiHttpClients\HttpClientInterface: '@Api\Tests\functional\Mock\ApiClientMock'
    

    现在,当我运行我的测试用例时,我没有看到像

    服务是私有的,您不能替换它。

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

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

    我知道我可以用 https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing

    更新 Api\Tests\functional\Mock\ApiClientMock

    “Api\Tests\functional\Mock\ApiClientMock”服务是合成的,需要在启动时设置它才能使用。

    1 回复  |  直到 6 年前
        1
  •  5
  •   Bogdan Dubyk    7 年前

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

    例如,我有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;
    }
    

    而他的客户则非常热衷于:

    <?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 我的所有服务都标记为私人服务:

            services:
               _defaults:
                    autowire: true
                    autoconfigure: true
                    public: false 
    
    
     App\Infrastructure\HttpClients\Adapters\Guzzle\GuzzleApiClient:
        shared: false
    

    service_test.yml 并将所有服务都设置为public,我需要创建stub类,它应该实现 HttpClientInterface 而且还能够模拟请求并将其与 HttpClient接口 services_test.yml .

    服务测试.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'
    

    <?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 implement 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,因为我们需要实现它 命令测试器 正如我在开头提到的。为此,我需要扩展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');
    

    这是简化版,我可能错过了一些东西,请随时询问,如果你需要一些解释!