Testing APIs

(with Behat and PHPUnit)

Omni Adams

What you'll (hopefully) learn

What you'll (accidentally) learn

What you'll (probably not) learn

What you'll (definitely not) learn

Testing with PHPUnit and cURL

FoxyCart

APIs versus IPAs

IPA

Testing with PHPUnit and cURL

function testBareGet() {
    $ch = curl_init(
        'https://api-sandbox.foxycart.com'
    );
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_exec($ch);
    $info = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $this->assertEquals(400, $info);
}

HTTP Status Codes

1xx
more information
2xx
it's all good
3xx
you're lost, let us help
4xx
you really messed up
5xx
not your fault, we broke things

Testing with PHPUnit and cURL

function testBareGet() {
    $ch = curl_init(
        'https://api-sandbox.foxycart.com'
    );
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_exec($ch);
    $info = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $this->assertEquals(400, $info);
}

Testing with PHPUnit and cURL

function testContentTypeOnError() {
    $ch = curl_init(
        'https://api-sandbox.foxycart.com/');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_exec($ch);
    $info = curl_getinfo($ch,
        CURLINFO_CONTENT_TYPE);
    $this->assertEquals(
        'application/vnd.error+json', $info);
}

Testing with PHPUnit and cURL

function testCacheControlHeader() {
    $ch = curl_init(…);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_setopt($ch, CURLOPT_HEADER, true);
    $output = curl_exec($ch);
    $this->assertContains('Cache-Control: no-cache',
        $output);
}

Testing with PHPUnit and cURL

function testCacheControlHeader() {
    $ch = curl_init(…);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_setopt($ch, CURLOPT_HEADER, true);
    $output = curl_exec($ch);
    $this->assertContains('Cache-Control: no-cache',
        $output);
}

Testing with PHPUnit and cURL

public function testOutputContainsErrorMessage() {
    $ch = curl_init(…);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    $output = curl_exec($ch);
    $this->assertContains(
        'FOXYCART-API-VERSION request header',
        $output);
}

Testing with PHPUnit and cURL

public function testOutputAsJson() {
    $ch = curl_init(…);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    $output = curl_exec($ch);
    $output = json_decode($output);
    $this->assertContains(
        'FOXYCART-API-VERSION request header',
        $output[0]->message);
}

Testing with PHPUnit and cURL

public function testRedirect() {
    $ch = curl_init(REDIRECT_URL);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_exec($ch);
    $info = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $this->assertEquals(302, $info);
}

Testing with PHPUnit and cURL

public function testRedirectNoFollow() {
    $ch = curl_init(REDIRECT_URL);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION,
        false);
    curl_exec($ch);
    $info = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $this->assertEquals(302, $info);
}

Testing with PHPUnit and cURL

public function testRedirectWithFollow() {
    $ch = curl_init(REDIRECT_URL);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION,
        true);
    curl_exec($ch);
    $info = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $this->assertEquals(302, $info);
}

Testing with PHPUnit and cURL

public function testRedirectWithFollow() {
    $ch = curl_init(REDIRECT_URL);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER,
        true);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION,
        true);
    $output = curl_exec($ch);
    
}

Sample header output

    HTTP/1.1 302 Found
    Location: /

    HTTP/1.1 200 OK
    Last-Modified: Fri, 28 Mar 2014 00:44:43 GMT
    Content-Length: 1986
    Vary: Accept-Encoding
    Content-Type: text/html

Sample header output

    HTTP/1.1 302 Found
    Location: /

    HTTP/1.1 200 OK
    Last-Modified: Fri, 28 Mar 2014 00:44:43 GMT
    Content-Length: 1986
    Vary: Accept-Encoding
    Content-Type: text/html

Testing with PHPUnit and cURL

api-testing(200) /$ phpunit apiTest.php
PHPUnit 3.7.28 by Sebastian Bergmann.

.....

Time: 1.21 seconds, Memory: 2.75Mb

OK (5 tests, 5 assertions)

Testing with PHPUnit and cURL

SLOW

Avoiding collisions

Wall collision

Avoiding collisions

Works on my machine

Avoiding collisions

PuPHPet

Avoiding collisions

api-testing(310 vagrant) /$ vagrant status
Current machine states:

api                       running (virtualbox)
cache                     running (virtualbox)
db                        running (virtualbox)
log                       not created (virtualbox)
monitor                   not created (virtualbox)
nosql                     not created (virtualbox)
web                       running (virtualbox)

This environment represents multiple VMs. The VMs are all
listed above with their current state. For more information
about a specific VM, run `vagrant status NAME`.

Testing APIs with Behat

Behat

Testing APIs with Behat

Mink

Testing APIs with Behat

Mink
 
 

Testing APIs with Behat

You had me at hello world

HTTP Methods

GET POST
HEAD PUT
OPTIONS DELETE
TRACE "PATCH"
CONNECT

HTTP Methods

GET POST
HEAD PUT
OPTIONS DELETE
TRACE "PATCH"
CONNECT

HTTP Methods

GET POST
HEAD PUT
OPTIONS DELETE
TRACE "PATCH"
CONNECT

Testing APIs with Behat

    # features/ping-controller.feature
    Feature: Ping controller
        To allow API testing
        As a client
        I should be able to ping the API
    

Testing APIs with Behat

    Scenario: GET ping endpoint
        When I send a GET request to "/ping"
        Then the response code should be 200
        And the response should contain json with a recent timestamp
    

Testing APIs with Behat

use Behat\Behat\Context\ClosuredContextInterface;
use Behat\Behat\Context\TranslatedContextInterface;
use Behat\Behat\Context\BehatContext;
use Behat\Behat\Exception\PendingException;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';
    

Testing APIs with Behat

public function __construct(array $parameters) {
    $curl = new \Buzz\Client\Curl();
    $curl->setOption(CURLOPT_FOLLOWLOCATION,
        false);
    $browser = new \Buzz\Browser($curl);
    $this->useContext('api',
        new Behat\CommonContexts\WebApiContext(
            $parameters['base_url'], $browser
    ));
    

Testing APIs with Behat

public function __construct(array $parameters) {
    $curl = new \Buzz\Client\Curl();
    $curl->setOption(CURLOPT_FOLLOWLOCATION,
        false);
    $browser = new \Buzz\Browser($curl);
    $this->useContext('api',
        new Behat\CommonContexts\WebApiContext(
            $parameters['base_url'], $browser
    ));
    

Testing APIs with Behat

public function __construct(array $parameters) {
    $curl = new \Buzz\Client\Curl();
    $curl->setOption(CURLOPT_FOLLOWLOCATION,
        false);
    $browser = new \Buzz\Browser($curl);
    $this->useContext('api',
        new Behat\CommonContexts\WebApiContext(
            $parameters['base_url'], $browser
    ));
    

Testing APIs with Behat

        
        $this->getSubcontext('api')
            ->setPlaceHolder(
                'BASE_URL',
                $parameters['base_url']
            );
    }
    

Testing APIs with Behat

Scenario: GET ping endpoint
    When I send a GET request to "/ping"
    Then the response code should be 200
    And the response should contain json with a recent timestamp

Testing APIs with Behat

/**
 * @Given /^the response should contain json with a recent timestamp$/
 */
public function theResponseShouldContainJsonWithARecentTimestamp() {
    
    

Testing APIs with Behat

    
    $response = $this->getSubcontext('api')
        ->getBrowser()
        ->getLastResponse()
        ->getContent();
    $time = time();
    $response = json_decode($response, true);
    
    

Testing APIs with Behat

    
    assertGreaterThan(
        $time - 2000,
        $response['time']
    );
    assertLessThan(
        $time + 2000,
        $response['time']
    );
}
    

Testing APIs with Behat

api-testing(200) /$ ./vendor/bin/behat tests/functional/ping.feature
Feature: Ping controller
    To allow API testing
    As a client
    I should be able to ping the API

Scenario: GET ping endpoint
    When I send a GET request to "/ping"
    Then the response code should be 200
    And the response should contain json with a recent timestamp

1 scenario (1 passed)
2 steps (2 passed)
0m0.137s

Testing APIs with Behat

Scenario: OPTIONS ping endpoint
    When I send a OPTIONS request to "/ping"
    Then the response code should be 204
    And the response should be empty
    And the header 'Access-Control-Allow-Methods' should be 'GET,HEAD,OPTIONS,PATCH,POST,PUT'
    

Testing APIs with Behat

Scenario: OPTIONS ping endpoint
    When I send a OPTIONS request to "/ping"
    Then the response code should be 204
    And the response should be empty
    And the header 'Access-Control-Allow-Methods' should be 'GET,HEAD,OPTIONS,PATCH,POST,PUT'
    

Testing APIs with Behat

Scenario: OPTIONS ping endpoint
    When I send a OPTIONS request to "/ping"
    Then the response code should be 204
    And the response should be empty
    And the header 'Access-Control-Allow-Methods' should be 'GET,HEAD,OPTIONS,PATCH,POST,PUT'
    

Testing APIs with Behat

    /**
     * Asserts that the response is empty.
     * @Given /^the response should be empty$/
     */
    public function theResponseShouldBeEmpty() {
        $response = $this->getSubcontext('api')
            ->getBrowser()->getLastResponse()
            ->getContent();
        assertEquals('', $response);
    }

Testing APIs with Behat

/**
 * @Given /^the header \'([^\']*)\' should be \'([^\']*)\'$/
 */
public function theHeaderShouldBe($name, $value) {
    $headers = $this->getSubcontext('api')
        ->getBrowser()->getLastResponse()
        ->getHeaders();
    assertContains($name . ': ' . $value, $headers);
}

Testing APIs with Behat

/**
 * @Given /^the header \'([^\']*)\' should be \'([^\']*)\'$/
 */
public function theHeaderShouldBe($name, $value) {
    $headers = $this->getSubcontext('api')
        ->getBrowser()->getLastResponse()
        ->getHeaders();
    assertContains($name . ': ' . $value, $headers);
}

Testing APIs with Behat

Beecat

Testing APIs with Behat

Behat

Designing APIs with Behat

dis mite be gud time to switch to plan b

Designing APIs with Behat

Ize in teh PLANNING stage
Omni Adams
@omnicolor
Mashery
http://omni-spot.blogspot.com
https://joind.in/10811

/