Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use Guzzle mock handlers with EightPointsGuzzleBundle #195

Closed
rtfjr86 opened this issue Apr 21, 2018 · 28 comments
Closed

How to use Guzzle mock handlers with EightPointsGuzzleBundle #195

rtfjr86 opened this issue Apr 21, 2018 · 28 comments
Labels

Comments

@rtfjr86
Copy link

rtfjr86 commented Apr 21, 2018

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
Copy link
Member

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
Copy link
Member

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

@rtfjr86
Copy link
Author

rtfjr86 commented Apr 23, 2018

@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
Copy link
Author

rtfjr86 commented Apr 23, 2018

I'm looking to be able to do something like

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

on the line below.

$handler = new Definition(HandlerStack::class);

@lcp0578
Copy link

lcp0578 commented May 8, 2018

@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
Copy link
Member

gregurco commented May 8, 2018

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

@lcp0578
Copy link

lcp0578 commented May 8, 2018

@gregurco thanks

@lcp0578
Copy link

lcp0578 commented May 8, 2018

@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
Copy link
Member

gregurco commented May 9, 2018

@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
Copy link

lcp0578 commented May 9, 2018

@gregurco thank you very munch!

@rrajkomar
Copy link
Contributor

rrajkomar commented Sep 25, 2018

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
Copy link
Contributor

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

  • no code change to the bundle but requires to use a reference get of the config to change the handler for the stack at test start. (not really DRY)

  • easiest but least flexible would be to check the kernel.environment variable in the extension class to automatically choose MockHandler upon HandlerStack definition (createHandlerStack) if environment is test

  • add a "handler" option to the configuration which a set of allowed values (i.e. "default, "mock" and so on) to allow switching handlers depending on environment (it would also allow custom handlers)
    this requires changes to Configuration class and Extension class

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

@gregurco
Copy link
Member

@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
Copy link
Contributor

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
Copy link
Member

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
Copy link
Contributor

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 #195 (comment)

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
Copy link
Member

gregurco commented Oct 1, 2018

@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
Copy link
Contributor

rrajkomar commented Oct 2, 2018

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) : rrajkomar@29dee73

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
Copy link
Contributor

@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
Copy link
Member

gregurco commented Oct 8, 2018

@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
Copy link
Contributor

Mmm Not sure I understood your reply... 😕

@gregurco
Copy link
Member

gregurco commented Oct 8, 2018

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
Copy link
Contributor

rrajkomar commented Oct 8, 2018

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 😞

@gregurco
Copy link
Member

gregurco commented Oct 8, 2018

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

@rrajkomar
Copy link
Contributor

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

@rrajkomar
Copy link
Contributor

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
Copy link
Member

#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
Copy link
Contributor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants