How to write a clean, portable & testable HTTP Client

Say we have a client who has requested their app be given the ability to fetch data found at an endpoint. While there are a million ways to do this there are not many ways to do this well covering everything which is important..Lets take a look at a clean and tested HTTP GET Client.

Lets take a look at a simple first draft:

use Guzzle/Http/Client;

class Food {
    public function getFoodData($type = 'vegetables') {
        $ourUrl = ‘http://www.myfoods.com/’;
        $client = new Client($ourUrl);

        $request = $client->get(
            '/foods/' . $type,
            array('Auth-Key' => 'sdfsdfsd', 'Content-Type' => 'application/json'),
            array()
        );
        $response = $request->send();

        $array = $response->json();
        $json = json_encode($array);
        $object = json_decode($json);

        $productIds = array();
        foreach ($object->items as $item) {
            $productIds[] = $item->vegetableId;
        }
        return $productIds;
    }
}

The solution works and seems simple enough, we have made use of Guzzle’s HTTP Client plus we can specify the food type, so whats wrong with it?
A couple of issues include:

- untestable

- violates SRP

- tightly coupled client to request

- rigid (can’t add another easily)

Lets take another crack at it keeping in mind the above points.

Initially we will start with just a basic HTTP Client for a GET request.

use Guzzle\Http\Client;

class Http 
{
    public $baseUrl = null;
    public $client = null;
    public $headers = array();

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

    public function request($path) {
        $client = new Client($this->baseUrl);

        return $client->get(
            $path,
            $this->headers
        );
    }

}

Now we can build on this and using Composition (explained here) add a class which makes use of our Client.

class Food 
{
    public function getVegetableIds() {
        $foods = $this->getFood('vegetable');

        $productIds = array_map(function($item) {
            return $item['vegetableId'];
        }, $foods['items']);

        return $productIds;
    }

    public function getFoodClient () {
        $ourUrl = 'http://www.myfoods.com/';

        return new Http($ourUrl);        
     }

    public function getFood($type) {
        $client = $this->getFoodClient();

        $client->headers = array('Auth-Key' => 'jijoijoijoij');

        return $client->request('/foods/' . $type)->send()->json();
     }
}

This is not only much easier to read but also separates out our concerns so the HTTP Client can be used again for any other type of HTTP request but also the Food class can easily be expanded for other foods.

Another positive about this approach is it allows us to unit test efficiently.

Below are some tests written using PHPUnit for our HTTP Client.

class HttpTest extends PHPUnit_Framework_TestCase
{
    protected $_object;

    protected function setUp () {
        parent::setUp();

        $this->_ourUrl = 'http://www.myfoods.com/';
        $this->_object = new Http($this->_ourUrl);
    }

    public function testHttpRequest () {
        $header = array(
            'Auth-Key' => 'jooijoij'
        );
        array_push($this->_object->headers, $header);
        $request = $this->_object->request('/foods/vegetables');

        $this->assertAttributeEquals($this->_ourUrl, 'baseUrl', $this->_object);
        $this->assertInstanceOf('Guzzle\Http\Message\Request', $request);
        $this->assertCount(3, $request->getHeaders());
    }

    public function testHttpRequestWithNoHeaders () {
        $request = $this->_object->request('/foods/vegetables');

        $this->assertAttributeEquals($this->_ourUrl, 'baseUrl', $this->_object);
        $this->assertInstanceOf('Guzzle\Http\Message\Request', $request);
        $this->assertCount(2, $request->getHeaders());
    }
}

This is testing some functionality that we require from the our HTTP Client. It can easily be expanded.

Once the HTTP Client tests are done we can move on to the Food class tests which are below.

class FoodTest extends PHPUnit_Framework_TestCase 
{
    public function testGetVegProdIds () {
        $foodSource = $this->getMock(
            'Food',
            array('getFood')
        );

        $testParams = array(
            'items' => array(
                array('vegetableId' => '123'),
                array('vegetableId' => '456')
            )
        );

        $foodSource->expects($this->once())
            ->method('getFood')
            ->will($this->returnValue($testParams));

        $prodIds = $foodSource->getVegetableIds();

        $this->assertCount(2, $prodIds);
    }

    public function testGetFood () {

        // The Guzzle Mock and assert
        $mockGuzzleRequest = $this->getMock(
            'Guzzle\Http\Message\Request',
            array('send', 'json'),
            array('GET', 'MyClient')
        );
        $mockGuzzleRequest->expects($this->once())
            ->method('send')
            ->will($this->returnValue($mockGuzzleRequest));
        $mockGuzzleRequest->expects($this->once())
            ->method('json')
            ->will($this->returnValue('MyResponse'));

        // The Http Mock and assert
        $httpMock = $this->getMock(
            'Http',
            array('request'),
            array('herp')
        );
        $httpMock->expects($this->once())
            ->method('request')
            ->with($this->equalTo('/foods/vegetables'))
            ->will($this->returnValue($mockGuzzleRequest)); // Guzzle Mock handed

        // The Food Mock and assert
        $foodMock = $this->getMock(
            'Food',
            array('getFoodClient')
        );
        $foodMock->expects($this->once())
            ->method('getFoodClient')
            ->will($this->returnValue($httpMock)); // Http Mock handed

        // Run
        $food = $foodMock->getFood('vegetables'); // Use Food Mock

        $this->assertEquals('MyResponse', $food);
    }
}

This class tests all functionality found inside our Food class. We are mocking the Client as we already have tests for this. More on mocking inside PHPUnit here.

Overall we can see that the second more decoupled approach has more code but is a lot simple and has many added benefits which im sure will help it stand the test of time.

Leave a Reply