Testing API calls in PHP with Guzzle Mocks
With all this in mind, I decided to just mock the CouchDB API for the calls I expected to make to it. Guzzle makes this much easier than I expected and it means that I can test all kinds of edge cases without trying to make contrived sequences of operations. As a side effect, the tests will run a lot faster using mocks than if I were making actual API calls to a remote database!
Testing a GET Request
I started simple! When you make a GET request to CouchDB, it returns a string like this:
{"couchdb":"Welcome","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}
The PHP library accepts either a URL or a Guzzle client as arguments to its constructor, and then offers a getVersion()
method. We therefore create a mock client in Guzzle, preloaded with the mock responses that it should provide, and give this to the PHP library. The test code ends up looking like this:
1 2 3 4 5 6 7 8 9 10 11 12 | public function testGetVersion() { $db_response = new GuzzleHttp\Psr7\Response(200, [], '{"couchdb":"Welcome","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}'); $mock = new GuzzleHttp\Handler\MockHandler([ $db_response, $db_response ]); $handler = GuzzleHttp\HandlerStack::create($mock); $client = new GuzzleHttp\Client(['handler' => $handler]); // userland code starts $server = new \PHPCouchDB\Server(["client" => $client]); $this->assertEquals("2.0.0", $server->getVersion()); } |
In fact, the library also makes the GET /
request to the server to check it can connect, so we need to return the response twice: once when we create the PHPCouchDB/Server
object and again when we call getVersion()
. To do this, we first create the Response
object(s) to return (but this example needs the same response twice) and feed them to the MockHandler
. From this, we create a HandlerStack
that then becomes the Client
to pass in to the library itself.
The last two lines are the code that is really being tested: creating a Server object from a Client, and then seeing that getVersion()
does what we expect and that we can recover the value from it.
Complete aside: Setting up with an injectable Guzzle Client was important to me because there are so many possible ways to connect to CouchDB and secure that connection – implementing and testing each one would be time-consuming. This way, users can easily use the well-documented and totally capable Guzzle Client if there is any special setup. The CouchDB library just focuses on the CouchDB stuff!
A Sequence of Requests
When working on this stuff, I found few resources with complete examples so I’ll share one that’s a bit more involved in case it’s useful. When you delete a record from CouchDB, if you omit the version information or there is a never version of a document than the one you are deleting, an error occurs. The error is expected though so we need to write code to expect the error in our test!
This is a pretty involved set of requests that the mock will need:
- a simple GET response as above when we connect to the server
- some database details in a response when we select which database to use
- a successful response when a record is created
- we fetch the document after creating it to make sure it looks like we think it should (maybe one request too many here, but it’s there so we’re testing it!)
- the response for a failed delete: a 409 response code
The test code builds up this array of responses, sets up the mock, and then runs the userland code to connect to a database and then create and delete a record. We’re expecting the delete to fail so we need to handle it. Ready?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | <?php require __DIR__ . "/../../vendor/autoload.php"; use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Exception\RequestException; class DocumentTest extends \PHPUnit\Framework\TestCase { public function setUp() { // create the first request to check we can connect, can be added to // the mocks for any test that wants it $couchdb1 = '{"couchdb":"Welcome","version":"2.0.0","vendor":{"name":"The Apache Software Foundation"}}'; $this->db_response = new Response(200, [], $couchdb1); // offer a use_response for when selecting this database $egdb1 = '{"db_name":"egdb","update_seq":"0-g1AAAABXeJzLYWBgYMpgTmEQTM4vTc5ISXLIyU9OzMnILy7JAUklMiTV____PyuRAY-iPBYgydAApP5D1GYBAJmvHGw","sizes":{"file":8488,"external":0,"active":0},"purge_seq":0,"other":{"data_size":0},"doc_del_count":0,"doc_count":0,"disk_size":8488,"disk_format_version":6,"data_size":0,"compact_running":false,"instance_start_time":"0"}'; $this->use_response = new Response(200, [], $egdb1); $create = '{"ok":true,"id":"abcde12345","rev":"1-928ec193918889e122e7ad45cfd88e47"}'; $this->create_response = new Response(201, [], $create); $fetch = '{"_id":"abcde12345","_rev":"1-928ec193918889e122e7ad45cfd88e47","noise":"howl"}'; $this->fetch_response = new Response(200, [], $fetch); } /** * @expectedException \PHPCouchDB\Exception\DocumentConflictException */ public function testDeleteConflict() { $delete = '{"error":"conflict","reason":"Document update conflict."}'; $delete_response = new Response(409, [], $delete); $mock = new MockHandler([ $this->db_response, $this->use_response, $this->create_response, $this->fetch_response, $delete_response ]); $handler = HandlerStack::create($mock); $client = new Client(['handler' => $handler]); // userland code starts $server = new \PHPCouchDB\Server(["client" => $client]); $database = $server->useDB(["name" => "egdb"]); $doc = $database->create(["noise" => "howl", "id" => "abcde12345"]); $result = $doc->delete(); } } |
The interesting bit starts halfway down around line 32, with the test we’re interested in. A bunch of the mocked requests are used by other tests, so they are created in the shared setUp()
method, but we need to create the response to the failed DELETE
request with a 409 response code.
Line 36 sees the mockhandler, stack and client created with the list of responses outlined above. Basically this allows us to simulate every stage of the library’s operation to get to the point where we can call delete and it fails.
The test function ends with no assertion, which might look strange. In fact the library will detect the failure and throw an exception. In PHPUnit, we use annotations to specify which exception should be thrown by this code so look at line 30 to see that the DocumentConflictException
should be triggered during this test. Counterintuitively, if the exception occurs, the test passes – but if everything works brilliantly then the test fails! This makes complete sense when you think about it enough but can be confusing the first time so beware :)
PHP and CouchDB
If you’re interested in using CouchDB, or are already using it and have some time to try out the library and let me know what you like or don’t like, you can find the project including all the tests above on GitHub: https://github.com/ibm-watson-data-lab/php-couchdb
Pingback: No REST for Women : API articles written by women : Maverick Tester