Testing APIs with Codeception

Matthew Turland

Questions or Feedback?

Slides

Linked from either of these:

https://joind.in/15533

http://matthewturland.com/publications

I Work Here

When I Work

I Co/Wrote These

php|architect's Guide to Web Scraping with PHP
http://phparch.com
PHP Master: Write Cutting-Edge Code
http://sitepoint.com

Goals

Types of Tests

How many can you name?

Unit Tests

See also: Unit Tests

Functional Tests

See also: Functional Tests

Supported Frameworks

Web Service Tests

See also: Web Services

No Silver Bullet

"There are no solutions, only trade-offs." ~ Paul M. Jones

Modernizing Legacy Applications in PHP

Modernizing Legacy Applications in PHP by Paul M. Jones [PDF/iPad/Kindle]

Installing Codeception

Getting Help

See also: Commands

Bootstrap Command

Composer Autoloader


{
    "autoload": {
        "psr-4": {
            "Vendor\\Subnamespace\\": "src/"
        }
    }
    "autoload-dev": {
        "psr-4": {
            "Vendor\\Tests\\Subnamespace\\": "tests/"
        }
    }
}

Bootstrap File

// tests/_bootstrap.php

// Add the line below this one
require __DIR__ . '/../vendor/autoload.php';

Supported Test Formats

See also: Cept, Cest, and Test Formats

Cept Example


$I = new ApiTester($scenario);
$I->wantTo('retrieve a single question');
$I->amAuthenticated();
$I->sendGET('2/questions/2');
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$I->seeResponseContainsJson([
    'id' => 2,
    'text'=>'Are you feeling well?'
]);

Cest Example


class QuestionCest {
    public function _before(ApiTester $I) {
        // ...
    }
    public function _after(ApiTester $I) {
        // ...
    }
    public function getSingleQuestion(ApiTester $I) {
        // Cept contents without $I assignment go here
    }
    // More test methods go here
}

Creating a Test Suite

Test Suite Configuration


# tests/api.suite.yml
class_name: ApiTester
modules:
  enabled:
    - \Helper\Api
    # Add the lines below
    - REST:
        # Your base API URL
        url: http://host/api/
        # Can also be a framework module name
        depends: PhpBrowser
        # Limits PhpBrowser to JSON or XML
        part: Json

See also: REST module

Test Suite Bootstrap


// tests/api/_bootstrap.php
// Here you can initialize variables that will be
// available to your tests

Helper Class


// tests/_support/Helper/Api.php
namespace Helper;
// here you can define custom actions
// all public methods declared in helper class will be
// available in $I

class Api extends \Codeception\Module
{

}

Adding Helper Methods


// tests/_support/Helper/Api.php
namespace Helper;
class Api extends \Codeception\Module {
    public function amAuthenticated(
        $username = 'default_user'
    ) {
        // $token = ...
        $this
            ->getModule('REST')
            ->amBearerAuthenticated($token);
    }
}

Building Tester Files

Creating a Cest Class

codecept generate:cest api Thing

// tests/api/ThingCest.php
use ApiTester;
class ThingCest {
    public function _before(ApiTester $I) {
    }
    public function _after(ApiTester $I) {
    }
    // tests
    public function tryToTest(ApiTester $I) {
    }
}

Using Tester Methods


// tests/api/ThingCest.php
use ApiTester;
class ThingCest {
    public function createThing(ApiTester $I) {
        $I->amAuthenticated('johndoe');
        // ...
    }
}

REST Module: Requests


// Authentication
$I->amHttpAuthenticated('username', 'password');
$I->amDigestAuthenticated('username', 'password');
$I->amBearerAuthenticated('token');

// Headers
$I->haveHttpHeader('name', 'value');

REST Module: Requests


// Method / Parameters
$I->sendGET('path/relative/to/url' /* , array $params */);
$I->sendHEAD(...);
$I->sendOPTIONS(...);
$I->sendPOST(
    'url' /* ,
    array|JsonSerializable|string $params,
    array $files
*/);
$I->sendPUT(...);
$I->sendPATCH(...);
$I->sendDELETE(...);
$I->sendLINK(
    'url',
    [ 'linkEntry1', 'linkEntry2', /* ... */ ]
);
$I->sendUNLINK(...);

REST Module: Responses


$I->dontSeeResponseCodeIs(200);
$I->seeResponseCodeIs(200);

$I->dontSeeHttpHeader('name'/* , 'value' */);
$I->seeHttpHeader('name'/* , 'value' */);
$I->seeHttpHeaderOnce('name');
$value = $I->grabHttpHeader('name', true);
$values = $I->grabHttpHeader('name');

$I->seeResponseEquals('text');
$I->dontSeeResponseContains('text');
$I->seeResponseContains('text');
$response = $I->grabResponse();

REST Module: JSON


$I->seeResponseIsJson();

$I->seeResponseJsonMatchesXpath('//property');

$I->dontSeeResponseContainsJson(['property' => 'value']);
$I->seeResponseContainsJson(['property' => 'value']);

$I->dontSeeResponseJsonMatchesJsonPath('$.property'); // [1]
$I->seeResponseJsonMatchesJsonPath('$.property');
$data = $I->grabDataFromResponseByJsonPath('$.property');

$I->dontSeeResponseMatchesJsonType(['property' => 'type']); // [2]
$I->seeResponseMatchesJsonType(['property' => 'type']);

[1] Needs flow/jsonpath in composer.json
[2] See this manual page for type notation

REST Module: XML


$->seeResponseIsXml();

$I->dontSeeXmlResponseEquals('xml string');
$I->seeXmlResponseEquals('xml string');

$I->dontSeeXmlResponseIncludes('xml substring');
$I->seeXmlResponseIncludes('xml substring');

$I->dontSeeXmlResponseMatchesXpath('//element');
$I->seeXmlResponseMatchesXpath('//element');

$attribute = $I->grabAttributeFrom('//element', 'attribute');
$value = $I->grabTextContentFromXmlElement('//element');

Asserts Module


# tests/api.suite.yml
class_name: ApiTester
modules:
  enabled:
    # ...
    - Asserts

// tests/api/ThingCest.php
$response = $I->grabResponse();
$I->assertRegExp('/^foo/', $response);

See also: Asserts module

Putting It All Together


// tests/api/ThingCest.php
use ApiTester;
class ThingCest {
    public function createThing(ApiTester $I) {
        $I->wantTo('retrieve a single thing');
        $I->amAuthenticated();
        $I->haveHttpHeader('Content-Type', 'application/json');
        $I->sendPOST('thing', ['name' => 'Foo']);
        $I->seeResponseCodeIs(200);
        $I->seeResponseIsJson();
        $I->seeResponseContainsJson([
            'id' => 1,
            'name' => 'Foo'
        ]);
    }
}

Data Integration

1 Userland library
2 Core extension
3 PECL extension

Db Module


# codeception.yml
# ...
modules:
  config:
    Db:
      dsn: ''
      user: ''
      password: ''
      dump: tests/_data/dump.sql

See also: Db

Creating a Helper

codecept generate:helper Db

// tests/_support/Helper/Db.php
namespace Helper;
// here you can define custom actions
// all public methods declared in helper class will be
// available in $I

class Db extends \Codeception\Module
{

}

See also: Helpers

Custom Db Helper


class Db extends \Codeception\Module\Db {
    protected function cleanup() {
        // ...
        $dbh->exec('SET FOREIGN_KEY_CHECKS=0');
        $res = $dbh->query(
            "SHOW FULL TABLES WHERE TABLE_TYPE LIKE '%TABLE'"
        );
        foreach ($res->fetchAll() as $row) {
            $dbh->exec('TRUNCATE TABLE `' . $row[0] . '`');
        }
        $dbh->exec('SET FOREIGN_KEY_CHECKS=1;');
        // ...
    }
}

See also: Customizing Codeception Database Cleanup

Custom Helper Configuration


# tests/api.suite.yml
class_name: ApiTester
modules:
  enabled:
    - \Helper\Db
    # ...
codecept build

Database Seeding Strategies


use League\FactoryMuffin\Facade as FactoryMuffin;
$faker = Faker\Factory::create();
FactoryMuffin::define('Model_Login', [
    'first_name' => $faker->firstName,
    'last_name'  => $faker->lastName,
    'email'      => 'unique:safeEmail',
    'password'   => function() {
        return Model_Login::hash_password('password');
    },
]);

See also: Data

PHP Web Server

composer require --dev "codeception/phpbuiltinserver:^1"

# codeception.yml
extensions:
  enabled:
    - Codeception\Extension\PhpBuiltinServer
  config:
    Codeception\Extension\PhpBuiltinServer:
      # Required
      hostname: localhost
      port: 8000
      documentRoot: ../web
      # Optional
      router: ../web/app.php
      directoryIndex: app.php
      startDelay: 1
      phpIni: /path/to/php.ini

See also: tiger-seo/PhpBuiltinServer

Running Tests


# All suites
codecept run

# Only the api suite
codecept run api
codecept run tests/api/

# Only the ThingCest class in the api suite
codecept run api ThingTest.php
codecept run tests/api/ThingCest.php

# Only the createThing test in the ThingCest class
# in the api suite
codecept run tests/api/ThingCest.php:createThing

See also: Running Tests

Environments


codecept generate:env travis

# tests/_envs/travis.yml
# Overrides for settings in codeception.yml go here

codecept run --env travis

# Merge environments
codecept run --env db,travis

See also: Environments

Debugging


// tests/api/ThingCest.php
codecept_debug('foo');
codecept_debug(['foo' => 'bar']);
$object = new \stdClass;
$object->foo = 'bar';
codecept_debug($object);

codecept run --debug api ThingCest.php

Code Coverage

Example HTML coverage report

See also: Code Coverage

Code Coverage Integration


{
    "require-dev": {
        "codeception/c3": "^2"
    },
    "scripts": {
        "post-install-cmd": [
            "Codeception\\c3\\Installer::copyC3ToRoot"
        ],
        "post-update-cmd": [
            "Codeception\\c3\\Installer::copyC3ToRoot"
        ]
    }
}

// web/index.php
include __DIR__ . '/../c3.php';
// ...

Code Coverage Configuration


# codeception.xml
coverage:
  enabled: true
  c3_url: 'http://host/index.php/'
  # Optional
  whitelist:
    include:
      - app/*
    exclude:
      - app/cache/*
  blacklist:
    include:
      - app/controllers/*
    exclude:
      - app/cache/CacheProvider.php

Code Coverage Invocation


# HTML reports for humans
codecept run --coverage --coverage-html

# XML (Clover) reports for IDEs (e.g. PHPStorm)
# and CI servers (e.g. Jenkins)
codecept run --coverage --coverage-xml

Remote Code Coverage Configuration

Requires xdebug.remote_enable to be set to true


# codeception.yml
coverage:
  # If API is hosted on different machine than tests
  remote: true
  # Optional
  remote_context_options:
    http:
      timeout: 60
    ssl:
      verify_peer: false

Groups


// tests/api/ThingCest.php
class ThingCest {
    /**
     * @group thing
     */
    public function createThing() {
        // ...
    }
    // ...
}


codecept run -g thing

See also: Groups

Parallel Execution

See also: Parallel Execution

Resources

Where to Find Me

Me with my cat Igor

Feedback

Please rate my talk!

https://joind.in/15533

Also, check out the joind.in mobile apps!