loading...

How to create MockHandler of Guzzle6

hirak profile image Hiraku NAKANO ・4 min read

Guzzle, which is a PHP HTTP client library, makes it possible to separate the portion that actually makes an HTTP request as a handler, and by replacing it, you can create a mock.

Even if you do not throw a request to the actual API server at the time of testing, you can declare that you received the response you wanted and can execute the code.

Guzzle official MockHandler

Guzzle provides MockHandler as test handler.
http://docs.guzzlephp.org/en/latest/testing.html#mock-handler

<?php
# Contents copied from the official
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Exception\RequestException;

// Create a mock and queue two responses.
$mock = new MockHandler([
    new Response(200, ['X-Foo' => 'Bar']),
    new Response(202, ['Content-Length' => 0]),
    new RequestException("Error Communicating with Server", new Request('GET', 'test'))
]);

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

// The first request is intercepted with the first response.
echo $client->request('GET', '/')->getStatusCode();
//> 200
// The second request is intercepted with the second response.
echo $client->request('GET', '/')->getStatusCode();
//> 202

If you pass a list of response objects like this, the configured responses will be returned in order from the top.
Then replace it with this $client (DI or something), just do a test.

What is different from making a real mock API server?

It is easy to create a real mock API, if it is OK with reasonable quality. Now you have php -S, nodejs and golang. So what's the merit of using mocking with Guzzle's mechanism?

pros

  • Execution is superfast (it is natural because it is completed in the same process)
  • You do not have to think about how to start a mock server or port number
  • Development cost is low
  • You can test with Guzzle's Middleware enabled at all

cons

  • Guzzle only
  • It is impossible to use it even if there are places where curl is used directly or file_get_contents is used
  • Since it is PHP after all, the handling of the stream is tremendously difficult (such as reproducing the way it flows very closely)

Make your own MockHandler

By the way, do you not think that Guzzle's official MockHandler is hard to use?
Since it is only possible to return responses in order from the list, if there are many parts hitting the same API, you have to keep it on the list many times accordingly.

So, I tried to make this handler part. Documents are scanty, but the amount of code to write is small.

Guzzle handler overview

Guzzle's handler can be anything that is callable.
However, as it becomes complicated, it is recommended to make it a class that implements the __invoke method
Take 2 arguments. Request and two other options.
Returns GuzzleHttp\Promise\PromiseInterface.

Promise will pass the response object if it is fulfilled.
If you do not throw an exception and you want to tell something an error, return it by wrapping the exception object in RejectedPromise.

<?php
use Psr\Http\Message\RequestInterface;
use GuzzleHttp\Promise;
use GuzzleHttp\Psr7;

class MyMock
{
    public function __invoke(RequestInterface $req, array $options)
    {
        return new Promise\FulfilledPromise(
            new Psr7\Response(200, ['X-Header' => 'hoge'], 'body string')
        );
    }
}

For example, in the above implementation, it is a setting that always returns body string at 200 regardless of any request.

use

Just pass it as an argument of \GuzzleHttp\HandlerStack::create.

<?php
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

$mock = new MyMock;

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

The $client is now ready for any requests but 200 returns.

To build in

The handler receives the RequestInterface of PSR - 7. If you analyze this and create things that dynamically respond, you can get closer to the behavior of the home API.

For example, by looking at the path and making it dynamically different.

<?php
//...
public function __invoke(RequestInterface $req, array $options)
{
    $path = $req->getUri()->getPath();
    switch ($path) {
        case '/foo':
            $body = 'foo!';
            break;
        case '/baa':
            $body = 'baa!';
            break;
        default:
            $body = '?!?!?!'
    }
    return new Promise\FulfilledPromise(
        new Psr7\Response(200, ['X-Header' => 'hoge'], $body)
    );
}

Or parse body and take parameters.

<?php
//...
public function __invoke(RequestInterface $req, array $options)
{
    // cast as string because it is troublesome as it is still stream (omission)
    $body = (string)$req->getBody();
    parse_str ($body, $params);
    return new Promise\FulfilledPromise(
        new Psr7\Response(200, ['Content-Type' => 'application / json'], json_encode ($params))
    );
}

You can build it easily.
PSR-7 may be a good idea of ​​what you can do with RequestInterface.

PSR-7: HTTP message interfaces - PHP - FIG

In addition, you can do something like setting a suitable setter + property for MyMock class so that you can set the response from outside.

Generate an error

A convenient part of the mock is that it is easy to create situations where replay is somewhat troublesome if it is a real API.
It is possible to return all errors such as connection errors and SSL certificate errors as well as 500 series communication errors.

Since Guzzle is completely Promises / A + in its internal structure, notification of errors uses Promise, not an exception mechanism. Still, you can not throw exceptions, there is nothing wrong with making exceptions.

Just set return new RejectedPromise($e) instead of throw.

<?php
// ...
public function __invoke(RequestInterface $req, array $options)
{
    return new Promise\RejectedPromise(
        new \GuzzleHttp\Exception\ConnectException(
            'Server is down'
            $req
        )
    );
}

This always results in a ConnectException. It is also free to make it according to the situation.

Impressions

Guzzle seems to be a framework made from middleware and handler rather than Http client.
Because the interior is Promise, I have a bit of difficulty getting used to how to practice, but I think that if you get used to it you will be well done.

Posted on by:

hirak profile

Hiraku NAKANO

@hirak

A Backend Engineer in Mercari-JP.

Discussion

markdown guide
 

thank you so much for this article, was struggling to get the request part included in my response. Was of great help.