Integration Testing with PHP-VCR

Integration testing with external webservices, has historically been an uncomforable process in PHP for me. It frequently involves complicated mocking that was fragile and hard to maintain. I’ve long wished for a PHP library that was as simple to use as HTTPretty is in Python. While building Stickler-CI I really wanted to have confidence in the user flows that interact with the GitHub and Stripe APIs. After some searching & recommendations on Twitter, I found PHP-VCR. It is a port of the VCR project from Ruby. It works by replacing the http/https stream handlers, and monkey patching curl through a PHP_User_Filter that rewrites PHP code as it is included.

Installation & Setup

After reading though parts of PHP-VCR I was ready to give it a shot. The documentation is a bit light, so I had to reference the library code, and fix a bug to make PHP-VCR compatible with CakePHP’s Http\Client. First up is installing PHP-VCR and its PHPUnit hooks:

Show Plain Text
  1. composer require --dev "php-vcr/php-vcr:^1.3.2"
  2. composer require --dev "php-vcr/phpunit-testlistener-vcr:^2.0"

Next, I had to add the test listeners to my phpunit.dist.xml or phpunit.xml file:

Show Plain Text
  1. <!-- In phpunit.xml.dist -->
  2. <!-- Setup a listener for fixtures -->
  3. <listeners>
  4.     <listener
  5.         class="PHPUnit_Util_Log_VCR"
  6.         file="vendor/php-vcr/phpunit-testlistener-vcr/PHPUnit/Util/Log/VCR.php" />
  7.     <!-- Other listeners -->
  8. </listeners>

Next I had to update my tests/bootstrap.php file to configure VCR:

Show Plain Text
  1. // In tests/bootstrap.php
  2. use VCR\VCR;
  3.  
  4. // Configure PHP-VCR
  5. VCR::configure()
  6.     ->setCassettePath(__DIR__ . '/Fixture/vcr')
  7.     ->setStorage('yaml')
  8.     ->setMode('once');

This code configures VCR to save ‘cassettes’ (stubbed request files) into the named directory, using the YAML format. I find the YAML storage easier to work with than JSON. We’ve also told VCR that we only want it to write to empty cassettes with setMode(). This prevents us from accidentally modifying existing fixtures.

Usage

Once VCR is wired up, it can be used in a test method by adding an annotation. An example test method from Stickler-CI is:

Show Plain Text
  1. /**
  2.  * @vcr controller_repositories_toggle_enable.yaml
  3.  */
  4. public function testToggleEnable()
  5. {
  6.     $this->post('/repositories/1/toggle', ['enable' => true]);
  7.     $this->assertResponseOk();
  8.     $this->assertJsonResponseEquals(['success' => true, 'enabled_count' => 1]);
  9.  
  10.     $repos = TableRegistry::get('Repositories');
  11.     $repo = $repos->get(1);
  12.     $this->assertTrue($repo->enabled, 'State should persist');
  13. }

You might notice there are no mocks setup here, allowing the test method to remain very readable. Behind the scenes, the vcr annotation enabled PHP-VCR augmenting the HTTP stream handlers. When the named fixture file is empty, PHP-VCR will append each un-matched request is sent to the webservice, and the request/response are then recorded in the cassette. Once complete, VCR will use the cassette to match requests and their responses; any request not recorded in the cassette will cause tests to fail. This is ideal as it lets me have confidence that none of the request parameters I am sending to webservices have changed, and that no new requests have been added.

Once complete, cassette files let you do a few interesting things:

  1. You can have confidence that your request parameters haven’t changed.
  2. You aren’t making more or fewer requests to webservices you consume.
  3. You can edit the cassette files to simulate edge cases and invalid responses from the webservices you consume.

My Workflow for Building Tests with VCR

After building several tests against the Github & Stripe APIs, I’ve come up with the following workflow:

  1. Write the test code, and get a failing assertion.
  2. Add the annotation to the test file using a filename that combines the test class, and scenario.
  3. Call \VCR\VCR::configure()->setMode('new_episodes'); so that new requests are appended.
  4. Run the test.
  5. Edit the cassette file
  6. Repeat the previous 2 steps until the test passes.
  7. Remove the setMode() call.

This workflow lets me iteratively build up both the cassette file and test case. I am really happy with this workflow as I am able to test my application code including client libraries all the way up to the network boundary. By testing the complete application, I get a higher degree of confidence that my code works over mock object based approaches ever did.

Comments

This is a great text. I also integrate VCR in my project. Woooohoooo

gokisa lokisa on 10/11/19

Have your say: