8p / EightPointsGuzzleBundle

⛽️ Integrates Guzzle 6.x, a PHP HTTP Client, into Symfony
MIT License
440 stars 71 forks source link

How to use Guzzle mock handlers with EightPointsGuzzleBundle #195

Closed rtfjr86 closed 6 years ago

rtfjr86 commented 6 years ago

I'm using Behat for testing. My application uses Guzzle to consume other services. When testing, I'm replacing the client in the container with my client that has a mock handler. It works fine until my test makes a second request, then it seems my client is replaced with the original. Is there a way to ensure that a mocked response is always returned when testing? Thanks.

FeatureContext.php $this ->getSession() ->getDriver() ->getClient() ->getContainer() ->set( 'eight_points_guzzle.client.products_service', $this->getMockClient($responses) );

test.feature When I go to "/some/url" And I reload the page

The first request returns the mocked response. The second makes a cURL request.

gregurco commented 6 years ago

Hello @rtfjr86 . Do you expect different responses for the same client in tests or there is always the same response? If the same then there is easy solution.

gregurco commented 6 years ago

@rtfjr86 did you see this repository https://github.com/FriendsOfBehat/ServiceContainerExtension ? Does it help you?

rtfjr86 commented 6 years ago

@gregurco Thank for the quick response. I know that the guzzle mock handler can be have multiple responses in a queue. I expect that each of those responses have a different body. I'd like to do something like:

Given the products service returns the following: | code | body
| 200 | {"products": {"product1": {"name": "test", "status": "enabled"}, "product2": {"name": "test2", "status": "enabled"}}} | 200 | {"product": {"name": "test", "status": "enabled"}}

I am trying to set theses responses in the guzzle mock handler. It seems to work for the first request, but the second is using the curl handler.

I will definitely take a look at https://github.com/FriendsOfBehat/ServiceContainerExtension. Thanks.

(please forgive my code formatting 😅)

rtfjr86 commented 6 years ago

I'm looking to be able to do something like

$handler = new Definition(HandlerStack::class, ['@my_handler']);

on the line below.

https://github.com/8p/EightPointsGuzzleBundle/blob/1f4c90037ba313fcee8e66b477bf31f272cbcc5f/src/DependencyInjection/EightPointsGuzzleExtension.php#L122

lcp0578 commented 6 years ago

@rtfjr86 @gregurco how can define a handle for a client? like this? thanks

$client = new Client([
    'base_uri' => "http://example.com",
    'handler' => $this->createLoggingHandlerStack([
        '{method} {uri} HTTP/{version} {req_body}',
        'RESPONSE: {code} - {res_body}',
    ]),
]);

I want log request body for debug.

gregurco commented 6 years ago

@lcp0578 just create listener and listen eight_points_guzzle.post_transaction event. See: https://github.com/8p/EightPointsGuzzleBundle#listening-to-events

lcp0578 commented 6 years ago

@gregurco thanks

lcp0578 commented 6 years ago

@gregurco

<?php
namespace ApiBundle\EventListener;

use EightPoints\Bundle\GuzzleBundle\Events\GuzzleEventListenerInterface;

class RequestListener implements GuzzleEventListenerInterface
{
    private $client;

    public function setServiceName($serviceName)
    {
        dump($serviceName);
        $this->client = $serviceName;
    }

    public function onPostTransaction()
    {
        // @todo
        dump('onPostTransaction');
        dump($this->client);
    }
}

services.yml

api.request_listener:
        class: ApiBundle\EventListener\RequestListener
        tags:
            - { name: kernel.event_listener, event: eight_points_guzzle.post_transaction, method: onPostTransaction, service: 'eight_points_guzzle.clients.api_department' }

eight_points_guzzle.clients.api_department is a client service id. but i don't know,how get eight_points_guzzle.clients.api_department reuqest info, I want log it.

Can you give me more hints about how to implement RequestListener? thx

gregurco commented 6 years ago

@lcp0578 try next way:

namespace ApiBundle\EventListener;

use EightPoints\Bundle\GuzzleBundle\Events\GuzzleEventListenerInterface;
use EightPoints\Bundle\GuzzleBundle\Events\PostTransactionEvent;

class RequestListener implements GuzzleEventListenerInterface
{    
    public function onPostTransaction(PostTransactionEvent $event)
    {
        if ($event->getServiceName() === 'api_department') {
            // do some logic
        }
    }
}
lcp0578 commented 6 years ago

@gregurco thank you very munch!

rrajkomar commented 6 years ago

Hello, I'd like to readress the initial question of this issue : how can I use the Mock Handler in my EightPointsGuzzleBundle Client class. I have a custom client class (defined as explained in https://github.com/8p/EightPointsGuzzleBundle/blob/master/src/Resources/doc/redefine-client-class.md) but I have a special requirement. I need for my tests to be able to set the MockHandler as the handler to be used for my custom client.

Would it be possible to add an option in the configuration to choose a specific handler to use ?

rrajkomar commented 6 years ago

As far as I could see, there seems to be three possible ways :

Any feedback on those ideas would be nice to start thinking about its implementation.

gregurco commented 6 years ago

@rrajkomar sorry for the delay. Could you please explain why not to mock the whole client and just the HandlerStack? I guess it's better and then you get the full access on call of methods and so on.

rrajkomar commented 6 years ago

I could mock the clients... if I knew how to do it :-) The question from a use case where I'm writing a webservice client api in a symfony bundle and I need to mock api calls to the webservice. I need to either be able to replace the client with a mock in test environment or to be able to tell the actual clients to use a guzzle mock handler.

gregurco commented 6 years ago

If you're using SF 4 then just create config file config/services_test.yaml. This file will be automatically loaded by symfony only in test env. In this file you will have possibility to redefine any services. After that you can create "mock class" in tests/Mocks folder and redefine client in config/services_test.yaml:

services:
    eight_points_guzzle.client.client_name:
        class: App\Tests\Mocks\YourApiClientName

Also you have another possibility to replace service in container in tests env. Just write next functionality in your setUp method:

    protected function setUp(): void
    {
        parent::setUp();

        $client = \Mockery::mock(Client::class);
        // define mocked methods

        static::$container->set('eight_points_guzzle.client.client_name', $client);
    }
rrajkomar commented 6 years ago

I don't like the first solution as it requires me to add some more classes for the mocks (while I won't be needing them) : I'm already using custom classes for the actual clients.

The second solution is better but requires me to write several lines of code to replace each client by a mock...

I was rather expecting a solution like this (which I thought about yesterday) : In test environment I'd define a service for HandlerStack and for MockHandler and then modify the guzzle clients configuration to use the same classes as in dev/prod but with a customized constructor argument : the handler stack.

somewhat like what was suggested in https://github.com/8p/EightPointsGuzzleBundle/issues/195#issuecomment-383716966

That way no need to write multiple lines of code and the configuration remains simple the handler argument for the client could be set to be an externally defined service.

gregurco commented 6 years ago

@rrajkomar got it and it makes sense. If you will give me example of your "mock of handler stack" then I can help you with adjustments on bundle's side.

rrajkomar commented 6 years ago

I'll create a fork of the project and make the changes there then link it back here so you can see what I had in mind.

@gregurco : Here you can see what I was thinking of more or less. (sorry didn't have time to write the tests but this should give an idea) : https://github.com/rrajkomar/EightPointsGuzzleBundle/commit/29dee7319ad4c74339bddeac87f64ccce3ba8752

I only configured the mockhandler to be integrated, not sure whether the complete choice (using the options entry) should be given, because the handler can be any callable, but I'm not sure how Definition will react if something else than a class name is provided.

It may be best to only set a boolean node mock_api_calls: T/F instead of allowing a direct setting of the handler class... Also the topic is to simplify the mock of the calls and the MockHandler should be sufficient.

rrajkomar commented 6 years ago

@gregurco : I've been working on the issue again during the week end. I'm close to something that works (with Tests and all) but I hit a wall because of the HandlerStack Definition in the Extension class.

It seems the Definition is created only once and not one per client with is problematic because if you have multiple clients, you cannot define a specific handler for only one client.

Any ideas on how to work past this ? See : https://github.com/rrajkomar/EightPointsGuzzleBundle (Note : there is still some work to be done on it before we can get it useable though)

gregurco commented 6 years ago

@rrajkomar yep, you are right. I think there is no restriction to change the definition of handler and you can change that one client will have one separate handler. What do you think?

rrajkomar commented 6 years ago

Mmm Not sure I understood your reply... :confused:

gregurco commented 6 years ago

For now all clients have one handler and it creates problems if we want to redefine/mock just one handler for specific client. And I guess in this case we have only one solution: to create separate handler for each client. Write me if it's clear now.

rrajkomar commented 6 years ago

Yes that's, what I started workign on locally but got stuck with the issue that it does not seem to be possible to create multiple definitions with the same "target" class. Or I missed something somewhere.

Here's what I wrote on my dev :

$handlerStackServiceName = sprintf('eight_points_guzzle.handler_stack.%s', $clientName);
$container->setDefinition($handlerStackServiceName, new Definition(HandlerStack::class));
$handler = $container->getDefinition($handlerStackServiceName);

then I tried

if (!$container->hasDefinition('eight_points_guzzle.handler_stack')) {
    $container->setDefinition('eight_points_guzzle.handler_stack', new Definition(HandlerStack::class));
}
$handlerStackServiceName = sprintf('eight_points_guzzle.handler_stack.%s', $clientName);
$container->setDefinition($handlerStackServiceName, new ChildDefinition('eight_points_guzzle.handler_stack'));
$handler = $container->getDefinition($handlerStackServiceName);

but neither worked :disappointed:

gregurco commented 6 years ago

https://github.com/rrajkomar/EightPointsGuzzleBundle/blob/master/src/DependencyInjection/EightPointsGuzzleExtension.php#L78 - I think here we should be returned Reference and not Definition

rrajkomar commented 6 years ago

I'll try this evening and get back to you.

rrajkomar commented 6 years ago

Here you go. In the end the issue was not between Definition or Reference but rather that the handler option got overwritten by the foreach on $options

gregurco commented 6 years ago

221 was merged. I think this issue can be closed and next step will be to write documentation described in task #177 . Subscribe to it to be announced when documentation will be implemented.

rrajkomar commented 6 years ago

Actually, I've already took the liberty of updating the README.md file to add the related documentation. Unless you want to put a specific page with a more detailed example.