Ashraam's avatar
Level 41

How could I mock Guzzle in my class ?

I building a wrapper for an API and I want to create some test for it.

Right now here is my class

class MyApi
{
    private $token;

    private $client;

    public function __construct($token)
    {
        if (!is_string($token)) {
            throw new InvalidArgumentException("The token must be a string");
        }

        $this->token = $token;

        $this->client = new Client([
            'base_uri' => 'https://url.com/api/v2/',
            'headers' => [
                'Content-type' => 'application/json',
                'Authorization' => "Bearer {$this->token}"
            ]
        ]);
    }
}

my test look like this

class BasicTest extends TestCase
{
    /** @test */
    public function it_requires_a_token()
    {
        $this->expectException(\ArgumentCountError::class);

        $api = new MyApi();
    }

    /** @test */
    public function the_token_must_be_a_string()
    {
        $this->expectException(\InvalidArgumentException::class);

        $api =  new MyApi(['token']);

        $api =  new MyApi(123);

        $api =  new MyApi(true);
    }

    /** @test */
    public function a_wrong_token_returns_an_error()
    {
        $this->expectException(\GuzzleHttp\Exception\ClientException::class);

        $mock = new MockHandler([
            new Response(401, [], '{"detail":"Token non valide."}')
        ]);

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

        $api = new MyApi('my_token');
        
        $response = $api->getCustomers();
   }
}

obviously it can't work because the test is using the "good" client, not the mocked one.

For now I've added an optional argument in MyApi constructor to add another client, but is there anyway to replace it without creating a new argument ?

Thank you

0 likes
3 replies
martinbean's avatar

@ashraam You’re just instantiate the client in your constructor. You use mocks to mock dependencies. So re-factor your API class to receive a Guzzle client as a parameter and then you can pass a mock instead:

use Guzzle\ClientInterface;

class MyApi
{
    protected $client;

    public function __construct(ClientInterface $client)
    {
        $this->client = $client;
    }
}

Now your API class receives a Guzzle client. It doesn’t care if it’s a real one or a mocked one. You can pass a mock one in your tests:

$client = Mockery::mock(ClientInterface::class);

$api = new MyApi($client);

This is also where you would the service provider to also instantiate your MyApi class with a Guzzle client properly configured with your token:

$this->app->singleton(MyApi::class, function () {
    $client = new Client([
        'base_uri' => 'https://url.com/api/v2',
        'headers' => [
            'Accept' => 'application/json',
            'Authorization' => sprintf('Bearer %s', $this->app['config']['services.myapi.token']),
            'Content-Type' => 'application/json',
        ],
    ]);

    return new MyApi($client);
});
Ashraam's avatar
Level 41

I get your point @martinbean but right now my "api" works like this

$api = new MyApi('token');

$clients = $api->clients()->get();

So now I should add an extra step where the user will instantiate Guzzle on his own ?

Like this ?

$httpClient = new Client([....]);

$api = MyApi($httpClient);

$clients = $api->clients()->get();
martinbean's avatar

@ashraam You can’t mock dependencies if you’re not injecting dependencies and instead just new-ing them up in your class.

Re-factor your MyApi class to instead receive a well-constructed Guzzle instead. You can construct the Guzzle instance (with API key) using the service contained. Then, all someone needs to do to use the class is type-hint it in another class such as a controller:

class FooController extends Controller
{
    protected $myApi;

    public function __construct(MyApi $myApi)
    {
        $this->myApi = $myApi;
    }
}

No manually instantiating the class or manually passing the token now.

1 like

Please or to participate in this conversation.