Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit Testing Guzzle inside of Laravel Controller with PHPUnit

I'm not quite sure which way to approach unit testing in this scenario. None of the examples for unit testing Guzzle quite make sense to me how to implement in this scenario, or perhaps I'm just looking at it incorrectly all together.

The setup: Laravel 4.2 REST API - A controller method is using Guzzle in the method to request data from another api as follows:

<?php

class Widgets extends Controller {
    public function index(){
        // Stuff

        $client = new GuzzleHttp\Client();
        $url = "api.example.com";

        $response = $client->request('POST', $url, ['body' => array(...)]);

        // More stuff
    }
}

?>

I thought that I could do my unit test as follows, and all would just work.

function testGetAllWidgets(){
    $mock_response = array('foo' => 'bar');

    $mock = new MockHandler([
        new Response(200, $mock_response),
    ]);

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

    $response = $this->call('GET', '/widgets');

    // Do asserts, etc.
}

However, Guzzle is still making the actual HTTP requests to the external service. My guess was maybe someway of setting the Client creation in the Controller method to use the $handler, but I can't imagine that's the right way to do it. What am I missing?

Edit My solution ended up as follows:

This solution felt the most correct, and the Laravel way. (See IoC Containers)

I would add this above each api call (change the mock responses depending on how many external calls need to be mocked in the api call).

$this->app->bind('MyController', function($app){
    $response_200 = json_encode(array("status" => "successful"));
    $response_300 = json_encode("MULTIPLE_CHOICES");

    $mock = new MockHandler([
        new Response(200, [], $response_200),
        new Response(300, [], $response_300)
    ]);

    $handler = HandlerStack::create($mock);

    return new MyController(new Client(['handler' => $handler]));
});

$params = array();

$response = $this->call('PUT', '/my-route', $params);

And if the controller required the Guzzle client, I added this to the controller:

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

And would then use $this->client for all of the api calls.

like image 369
smb Avatar asked Oct 16 '15 03:10

smb


3 Answers

The "classic TDD" response to this would be that you shouldn't unit test Guzzle at all. Guzzle is a third-party library which should be (and is) tested perfectly adequately by its own developer.

What you need to test is whether your code correctly calls Guzzle, not whether Guzzle works when your code calls it.

The way to do this is as follows:

Rather than doing a new Guzzle() in your controller, you should instead pass a Guzzle object into your controller using dependency injection. Fortunately, Laravel makes this very easy; all you need to do is have a constructor method for your controller class, and have a Guzzle object defined as one of its arguments. Laravel will do the rest of creating the object and passing it in for you. Your constructor can then copy it to a class property so that your other methods can use it.

Your class should now look something like this:

class Widgets extends Controller {
    private $guzzle;
    public function __construct(GuzzleHttp\Client $guzzle)
    {
        $this->guzzle = $guzzle;
    }

    public function index(){
        // Stuff

        $url = "api.example.com";

        $response = $this->guzzle->request('POST', $url, ['body' => array(...)]);

        // More stuff
    }
}

Now your test should be a lot easier to write. You can pass a mock Guzzle object into your class when you test it.

Now you can just watch your mock class to make sure that the calls to it match what the Guzzle API would expect to recieve in order to make the call.

If the rest of your class depends on the output received from Guzzle then you can define that in your mock as well.

like image 148
Simba Avatar answered Oct 18 '22 02:10

Simba


Use https://github.com/php-vcr/php-vcr package. It helps to record and replay HTTP requests. It is very handy for testing api calls via Guzzle

like image 41
Maxim Lanin Avatar answered Oct 18 '22 01:10

Maxim Lanin


If anyone is struggling with this one then I found replacing:

$this->app->bind('MyController', function($app){

With

$this->app->bind(MyController::class, function($app){

Did the trick for me in Laravel 5.5.44

like image 1
Tom Metcalfe Avatar answered Oct 18 '22 01:10

Tom Metcalfe