3

I am trying to mock a guzzle response from a specific api.

My controller code looks like this (amended for brevity):

class SomeClass
{

    private $guzzle;

    public function __construct(\GuzzleHttp\Client $guzzle) {
        $this->guzzle = new $guzzle();
    }

    public function makeRequest(){

        $client = $this->guzzle;

        $url = 'http//somerurl';
        $options = [];

        $response = $client->request('POST', $url, $options);    

        return $response;
    }
}

And the test looks something like this (again edited)...

public function someTest(){

     $mock = $this->createMock(\GuzzleHttp\Client::class);

     $mock->method('request')->willReturn([
         'response' => 'somedata'
     ]);

     $someClass = new $SomeClass($mock);

     $response = $someClass->makeRequest();

     $body = $response->getBody();

     ...
}

At this point the test returns "Call to a member function getBody on null";

How can the getBody response of a guzzle call be tested?

Thank you in advance...

James Satori-Brunet
  • 901
  • 2
  • 13
  • 28

4 Answers4

5

One approach to testing with Guzzle is to configure a MockHandler

http://docs.guzzlephp.org/en/stable/testing.html

So instead of mocking the guzzle client, you create one like so:

public function someTest() {

    $mock = new MockHandler([
        new Response(200, [], 'The body!'),
        // Add more responses for each response you need
    ]);

    $handler = HandlerStack::create($mock);
    $client = new Client(['handler' => $handler]);

    $someClass = new SomeClass($client);

    $response = $someClass->makeRequest();

    $body = $response->getBody();

    $this->assertSame('The body!', $body);
}
Harry
  • 2,429
  • 4
  • 21
  • 26
  • And then what? Where does this code go? How does this fit in with what I am trying to achieve in the code I wrote? – James Satori-Brunet Mar 13 '19 at 17:05
  • The code lives in the test. You're creating the response instead of mocking it. If you want to use phpunit to mock then you'll want to do `$mock->method('request')->willReturn(new Response(200, [], 'hello'));` instead. – Harry Mar 13 '19 at 17:32
  • It's not working. Exactly the same error as before. – James Satori-Brunet Mar 13 '19 at 17:36
1

The MockHandler requires you to 'queue' responses, meaning you need to know in what order external API calls will be made. I've taken this a step further and wrapped the MockHandler in another handler capable of stuffing a dummy-response into it at the last moment, if one isn't already waiting in the wings. See https://gist.github.com/kmuenkel/d4d473beb7b2297ac2d8cd480089a738

Just use that trait in your test, and call $this->mockGuzzleResponses(); from the test class's setUp() method. At that point, all requests intended to pass through Guzzle will be available for assertions by way of the $guzzleRequestLog property, and all responses can be mocked by calling $this->guzzleHandler->append(RequestInterface); at the beginning of your test.

Just make sure that all implementations of Guzzle in your code are instantiated by way of the app(Client::class) helper and not new Client. Otherwise the binding override won't take effect. That may have been your issue earlier.

kmuenkel
  • 2,659
  • 1
  • 19
  • 20
0

Take a look at my composer package https://packagist.org/packages/doppiogancio/mocked-client.

In my opinion, it's a really simple way to mock a Guzzle Client, binding request URLs with responses.

$builder = new HandlerStackBuilder();

// Add a route with a response via callback
$builder->addRoute(
    'GET', '/country/IT', static function (ServerRequestInterface $request): Response {
        return new Response(200, [], '{"id":"+39","code":"IT","name":"Italy"}');
    }
);

// Add a route with a response in a text file
$builder->addRouteWithFile('GET',  '/country/IT/json', __DIR__ . '/fixtures/country.json');

// Add a route with a response in a string
$builder->addRouteWithString('GET',  '/country/IT', '{"id":"+39","code":"IT","name":"Italy"}');

// Add a route mocking directly the response
$builder->addRouteWithResponse('GET', '/admin/dashboard', new Response(401));

$client = new Client(['handler' => $builder->build()]);

Once you did you will have a fully functional client to use normally

$response = $client->request('GET', '/country/DE/json');
$body = (string) $response->getBody();
$country = json_decode($body, true);

print_r($country);

// will return
Array
(
    [id] => +49
    [code] => DE
    [name] => Germany
)
0

Another elegant solution I would go with would be:

    use Mockery;
    use GuzzleHttp\Psr7\Response;

    public function someTest(){

        $mock =  Mockery::mock(\GuzzleHttp\Client::class);

        $mock->shouldReceive('request')->andReturn([
           new Response(200, [], 'success')
        ]);

        $someClass = new $SomeClass($mock);

        $response = $someClass->makeRequest();

        $body = $response->getBody();
    }

If trying to handle an object which is supposed to be returned from Guzzle, you should first encode the array (or pass it as a string directly) as the third parameter inside Response constructor

        $mock->shouldReceive('request')->andReturn([
           new Response(200, [], json_encode(['success' => true]))
        ]);
        // logic
        $body = json_decode($response->getBody()->getContents(), true);
        $this->assertEquals(true, body['success']);
donald
  • 71
  • 3