KEMBAR78
Create, test, secure, repeat | PDF
Create, Test, Secure &
Repeat
Part of the in2it Quality Assurance Training
in it2PROFESSIONAL PHP SERVICES
https://www.flickr.com/photos/90371939@N00/4344878104
Michelangelo van Dam
PHP Consultant, Community Leader & Trainer
https://www.flickr.com/photos/akrabat/8784318813
Create, Test, Secure, Repeat
Workshop
Get prepared
https://github.com/in2it/ctsr-workshop
Requirements
5.4+
Requirements
- a computer with PHP 5.4 or higher
- the latest composer
- git
Exercises tested on
- Microsoft Windows 7 & 8
- Linux
- Mac OS X 10.10.2 or higher
Learn unit testing like a pro
https://www.flickr.com/photos/eriktorner/8048428729
Introduction
Mise en place (preparation)
Running Tests
Starting a new project with TDD
Testing & Improving legacy code
Other tools
Recap & Closing remarks
https://www.flickr.com/photos/ryantylersmith/14010104872
Introduction
Mise en place (preparation)
Running Tests
Starting a new project with TDD
Testing & Improving legacy code
Other tools
Recap & Closing remarks
https://www.flickr.com/photos/ryantylersmith/14010104872
PHPUnit
• Created by Sebastian
Bergmann in 2004
• Port of xUnit to PHP
• Uses assertions for testing
• Supports
• Unit testing
• Integration testing w/ DBUnit
• Acceptance testing w/
Selenium
https://www.flickr.com/photos/greenboy/3895219425
https://www.flickr.com/photos/greenboy/3895219425
https://www.flickr.com/photos/tonivc/
https://www.flickr.com/photos/greenboy/3895219425
https://www.flickr.com/photos/tonivc/
https://www.flickr.com/photos/68751915@N05/6736158045
https://www.flickr.com/photos/akrabat/8421560178
https://www.flickr.com/photos/jakerust/16223669794
A little test
Who created PHPUnit and is
now project lead?
A. Chris Hartjes
B. Sebastian Bergmann
C. Stefan Priepsch
With PHPUnit you can…?
A. Test the smallest functional piece of code (unit)
B. Test integrations with a database (integration)
C. Test automated acceptance testing with Selenium
D. All of the above
E. None of the above
An assertion is…?
A. Verifying that an expected value matches the
result of a process?
B. Verifying that a process produces results
C. A transformation of a value
Assertion
• is a true/false statement
• to match an expectation
• with the result of functionality under test
Available assertions
• assertArrayHasKey()
• assertArraySubset()
• assertClassHasAttribute()
• assertClassHasStaticAttribute()
• assertContains()
• assertContainsOnly()
• assertContainsOnlyInstancesOf()
• assertCount()
• assertEmpty()
• assertEqualXMLStructure()
• assertEquals()
• assertFalse()
• assertFileEquals()
• assertFileExists()
• assertGreaterThan()
• assertGreaterThanOrEqual()
• assertInstanceOf()
• assertInternalType()
• assertJsonFileEqualsJsonFile()
• assertJsonStringEqualsJsonFile()
• assertJsonStringEqualsJsonString()
• assertLessThan()
• assertLessThanOrEqual()
• assertNull()
• assertObjectHasAttribute()
• assertRegExp()
• assertStringMatchesFormat()
• assertStringMatchesFormatFile()
• assertSame()
• assertStringEndsWith()
• assertStringEqualsFile()
• assertStringStartsWith()
• assertThat()
• assertTrue()
• assertXmlFileEqualsXmlFile()
• assertXmlStringEqualsXmlFile()
• assertXmlStringEqualsXmlString()
Available assertions
• assertArrayHasKey()
• assertArraySubset()
• assertClassHasAttribute()
• assertClassHasStaticAttribute()
• assertContains()
• assertContainsOnly()
• assertContainsOnlyInstancesOf()
• assertCount()
• assertEmpty()
• assertEqualXMLStructure()
• assertEquals()
• assertFalse()
• assertFileEquals()
• assertFileExists()
• assertGreaterThan()
• assertGreaterThanOrEqual()
• assertInstanceOf()
• assertInternalType()
• assertJsonFileEqualsJsonFile()
• assertJsonStringEqualsJsonFile()
• assertJsonStringEqualsJsonString()
• assertLessThan()
• assertLessThanOrEqual()
• assertNull()
• assertObjectHasAttribute()
• assertRegExp()
• assertStringMatchesFormat()
• assertStringMatchesFormatFile()
• assertSame()
• assertStringEndsWith()
• assertStringEqualsFile()
• assertStringStartsWith()
• assertThat()
• assertTrue()
• assertXmlFileEqualsXmlFile()
• assertXmlStringEqualsXmlFile()
• assertXmlStringEqualsXmlString()
Example usage
Assertions
// Asserting a value returned by $myClass->myMethod() is TRUE
$this->assertTrue($myClass->myMethod());
// Asserting that a string matches type and value of $myClass->toString()
$this->assertSame('my string', $myClass->toString());
// Asserting that the value matches $myClass-
>addOne(0), type check not necessary
$this->assertEquals('1', $myClass->addOne(0));
// Assserting that the result of $myClass->getBirthday()->format('Y') 
// is greater than the expected value
$this->assertGreaterThan(1900, $myClass->getBirthday()->format('Y'));
// Asserting a value is NULL with a specified error message
$this->assertNull(
    $myClass->getProperty(), 
    'When instantiating MyClass the property value should be NULL but is 
' . $myClass->getProperty()
);
Annotations
• Provide automatic features
• to execute arbitrary functionality
• without having to create logic
Available annotations
• @author
• @after
• @afterClass
• @backupGlobals
• @backupStaticAttributes
• @before
• @beforeClass
• @codeCoverageIgnore*
• @covers
• @coversDefaultClass
• @coversNothing
• @dataProvider
• @depends
• @expectedException
• @expectedExceptionCode
• @expectedExceptionMessage
• @expectedExceptionMessageRegExp
• @group
• @large
• @medium
• @preserveGlobalState
• @requires
• @runTestsInSeparateProcesses
• @runInSeparateProcess
• @small
• @test
• @testdox
• @ticket
• @uses
@group
<?php
class OrderTest extends PHPUnit_Framework_TestCase
{
    /**
     * @group Order
     */
    public function testCanCreateOrder()
    {
        // ... test logic goes here
    }
    
    /**
     * @group Order
     * @group BUG-1234
     */
    public function testOrdersCanNotContainSoldProducts()
    {
        // ... test logic goes here
    }
}
How to use @group
# Run phpunit only against tests for the Order module
./vendor/bin/phpunit --group Order
# Run phpunit for all tests except for the Order module
./vendor/bin/phpunit --exclude-group Order
@dataProvider
<?php
class OrderTest extends PHPUnit_Framework_TestCase
{
    public function badDataProvider()
    {
        return [
            [0, 0, 0], // we don't accept ID's less or equal than 0
            ['', new stdClass(), []], // only integer and float values
            [null, null, null], // no NULL values allowed
        ];
    }
    
    /**
     * @dataProvider badDataProvider
     */
    public function testAddProductToOrder($orderId, $productId, $price)
    {
        $order = new Order();
        $result = $order->addProductToOrder($orderId, $productId, $price);
        $this->assertFalse($result);
    }
}
@expectedException
<?php
class OrderTest extends PHPUnit_Framework_TestCase
{
    /**
     * @expectedException InvalidArgumentException
     * @expectedExceptionMessage The order ID cannot be null
     */
    public function testAddProductToOrder()
    {
        $orderId = null;
        $productId = null;
        $price = null;
        $order = new Order();
        $result = $order->addProductToOrder($orderId, $productId, $price);
        $this->fail('Expected exception was not thrown');
    }
}
www.phpunit.de
Introduction
Mise en place (preparation)
Running Tests
Starting a new project with TDD
Testing & Improving legacy code
Other tools
Recap & Closing remarks
https://www.flickr.com/photos/ryantylersmith/14010104872
https://www.flickr.com/photos/427/2253530041
Getting PHPUnit
Composer way (preferred)
Download Composer

curl -sS https://getcomposer.org/installer | php
Get phpunit

php composer.phar require phpunit/phpunit
Direct download
curl -O https://phar.phpunit.de/phpunit.phar
https://www.flickr.com/photos/rueful/5906546599
For this training
Installation of source code
Clone the repository into your workspace before attending the workshop.

git clone https://github.com/in2it/ctsr-workshop.git
cd ctsr-workshop/
Once you have cloned the training package, make sure you install composer.

curl -sS https://getcomposer.org/installer | php
When the download is done, install required components using composer

php composer.phar install
For this training
Installation of source code
Clone the repository into your workspace before attending the workshop.

git clone https://github.com/in2it/ctsr-workshop.git
cd ctsr-workshop/
Once you have cloned the training package, make sure you install composer.

curl -sS https://getcomposer.org/installer | php
When the download is done, install required components using composer

php composer.phar install
https://www.flickr.com/photos/intelfreepress/13983474320
During the workshop you're asked to solve several exercises. All example codes
are based on a UNIX-like OS, so if you plan to participate this workshop with
another OS, you need to know what changes are required to have the exercises
run on your operating system.

The exercises, the source code and the examples are tested on the following
platforms:

	 •	 Windows 7

	 •	 Mac OS X

	 •	 Ubuntu Linux

When you need to switch to a specific exercise branch (e.g. ex-0.0), you can do
this with the following command.

git checkout -b ex-0.0 origin/ex-0.0
https://www.flickr.com/photos/rhinoneal/8060238470
phpunit.xml
<?xml	
  version="1.0"	
  encoding="utf-­‐8"?>	
  
<phpunit	
  
	
  	
  	
  	
  bootstrap="./vendor/autoload.php"	
  
	
  	
  	
  	
  colors="true"	
  
	
  	
  	
  	
  stopOnFailure="true"	
  
	
  	
  	
  	
  stopOnError="true"	
  
	
  	
  	
  	
  syntaxCheck="true">	
  
	
  	
  	
  	
  <testsuite	
  name="Unit	
  Tests">	
  
	
  	
  	
  	
  	
  	
  	
  	
  <directory>./tests</directory>	
  
	
  	
  	
  	
  </testsuite>	
  
	
  	
  	
  	
  <filter>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <whitelist>	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  <directory	
  suffix=".php">./src</directory>	
  
	
  	
  	
  	
  	
  	
  	
  	
  </whitelist>	
  
	
  	
  	
  	
  </filter>	
  
	
  	
  	
  	
  <logging>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <log	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  type="coverage-­‐html"	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  target="./build/coverage"	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  charset="UTF-­‐8"	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  yui="true"	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  highlight="true"	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  lowUpperBound="35"	
  
	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  	
  highLowerBound="70"/>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <log	
  type="coverage-­‐xml"	
  target="./build/logs/coverage.xml"/>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <log	
  type="coverage-­‐clover"	
  target="./build/logs/clover.xml"/>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <log	
  type="tap"	
  target="./build/logs/phpunit.tap"/>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <log	
  type="testdox-­‐text"	
  target="./build/logs/testdox.txt"/>	
  
	
  	
  	
  	
  	
  	
  	
  	
  <log	
  type="junit"	
  target="./build/logs/junit.xml"	
  logIncompleteSkipped="false"/>	
  
	
  	
  	
  	
  </logging>	
  
</phpunit>
Introduction
Mise en place (preparation)
Running Tests
Starting a new project with TDD
Testing & Improving legacy code
Other tools
Recap & Closing remarks
https://www.flickr.com/photos/ryantylersmith/14010104872
https://www.flickr.com/photos/cgc/6776701
Composer PHPUnit
./vendor/bin/phpunit
Phar PHPUnit
php phpunit.phar
Exercise 0.0
• What will happen when you run PHPUnit now?
• Checkout branch ex-0.0
git checkout -b ex-0.0 origin/ex-0.0
php composer.phar dump-autoload
• Run PHPUnit
Let’s write our first test
<?php
namespace In2itTestWorkshopCtsr;
use In2itWorkshopCtsrSampleClass;
class SampleClassTest extends PHPUnit_Framework_TestCase
{
    public function testSomethingReturnsGreeting()
    {
        $sampleClass = new SampleClass();
        $this->assertSame(
            'Hello World!', $sampleClass->doSomething()
        );
    }
}
Exercise 0.1
• What will happen when you run PHPUnit now?
• Checkout branch ex-0.1
git checkout -b ex-0.1 origin/ex-0.1
php composer.phar dump-autoload
• Run PHPUnit
Error?
Got Error?
error: Your local changes to the following files would be overwritten by checkout:
composer.lock
Please, commit your changes or stash them before you can switch branches.
Aborting
Solution
git checkout -- composer.lock
git checkout -b ex-0.1 origin/ex-0.1
Write our class
<?php
namespace In2itWorkshopCtsr;
class SampleClass
{
    public function doSomething()
    {
        return 'Hello World!';
    }
}
Exercise 0.2
• What will happen when you run PHPUnit now?
• Checkout branch ex-0.2
git checkout -b ex-0.2 origin/ex-0.2
php composer.phar dump-autoload
• Run PHPUnit
Exercise 0.3
• Test that you can provide an argument and the
argument will be returned as “Hello <arg>!”
• Write the test
• Modify the class
Modifying the test class
    public function testSomethingReturnsArgument()
    {
        $sampleClass = new SampleClass();
        $argument = 'Class';
        $this->assertSame(
            sprintf('Hello %s!', $argument),
            $sampleClass->doSomething($argument)
        );
    }
Modify the SampleClass
<?php
namespace In2itWorkshopCtsr;
class SampleClass
{
    public function doSomething($argument)
    {
        return 'Hello ' . $argument . '!';
    }
}
Update further
<?php
namespace In2itWorkshopCtsr;
class SampleClass
{
    public function doSomething($argument = 'World')
    {
        return 'Hello ' . $argument . '!';
    }
}
Chapter 0
What have you learned
• How to install phpunit
• How to configure phpunit
• How to write your test first
• How to modify requirements through testing
• How to debug failures and fix them easily with tests
https://www.flickr.com/photos/raster/3563135804
Introduction
Mise en place (preparation)
Running Tests
Starting a new project with TDD
Testing & Improving legacy code
Other tools
Recap & Closing remarks
https://www.flickr.com/photos/ryantylersmith/14010104872
https://www.flickr.com/photos/esoterika/5347998757
Schedule Manager
Functional requirements
• Application needs to manage cron entries, where
all scheduled tasks are stored in a database and
written to the crontab when saved.
What is crontab?
• A tool on UNIX-like systems to execute tasks at a certain interval
• Each line contains the following items:
• Minutes and/or interval
• Hours and/or interval
• Days of the month (DOM) and/or interval
• Months and/or interval
• Days of the week (DOW) and/or interval
• Executable command
Example crontab
# Minutes Hours DOM Months DOW Command
# Warm up caches with new products
# Every 5 minutes each day
*/5 * * * * /bin/sh /path/to/productCollector.sh 2>&1
# Send marketing mail to active customers
# Every Monday at 9:30am
30 9 * * 1 /usr/bin/php /path/to/marketingMailSender.php 2>&1
# Clean up waste
# Every day at 6am, 1pm and 5pm from Monday to Friday
0 6,13,17 * * 1-5 /bin/sh /path/to/wasteCleaner.sh 2>&1
Analysis
• crontab = collection of entries
• Each entry contains 5 assets and a command
• Each asset can contain a
• Wildcard (full range)
• Range n-m (subset of full range)
• List n,m,o
• A single value n
• With similar intervals (without the wildcard)
https://www.flickr.com/photos/caveman_92223/3968354387
• cronmanager
• collection of crontab entries
• each entry contains
• minutes (collection) and interval (collection)
• hours (collection) and interval (collection)
• days of the month (collection) and interval (collection)
• months (collection) and interval (collection)
• days of the week (collection) and interval (collection)
• command string
• each entry collection (minutes, hours, dom, months, dow) requires a range
• minutes (0 - 59)
• hours (0 - 23)
• days of the month (1 - 31)
• month (1 - 12)
• days of the week (0 - 7) (0 & 7 are Sunday)
• crontab is write-only, so we need to update the full crontab completely
<?php
namespace In2itTestWorkshopCtsr;
class CronManagerTest extends PHPUnit_Framework_TestCase
{
    public function testCronManagerCanAddAnEntry()
    {
        $entry = new stdClass();
        $cronman = new CronManager();
        $cronman->addEntry($entry);
        $this->assertCount(1, $cronman);
    }
}
<?php
namespace In2itWorkshopCtsr;
class CronManager implements Countable, Iterator
{
    protected $stack;
    protected $pointer;
    protected $counter;
    public function addEntry($entry)
    {
        $this->stack[] = $entry;
        $this->counter++;
    }
    // Implement the Iterator and Countable methods here.
    // - Iterator: http://php.net/manual/en/class.iterator.php
    // - Countable: http://php.net/manual/en/class.countable.php 
    public function current() {/**... */}
    public function next() {/**... */}
    public function key() {/**... */}
    public function valid() {/**... */}
    public function rewind() {/**... */}
    public function count() {/**... */}
}
<?php
namespace In2itTestWorkshopCtsr;
use In2itWorkshopCtsrCronManager;
class CronManagerTest extends PHPUnit_Framework_TestCase
{
    public function testCronManagerCanAddAnEntry()
    {
        $entry = new stdClass();
        $cronman = new CronManager();
        $cronman->addEntry($entry);
        $this->assertCount(1, $cronman);
    }
}
<?php
namespace In2itTestWorkshopCtsr;
use In2itWorkshopCtsrCronManager;
class CronManagerTest extends PHPUnit_Framework_TestCase
{
    public function testCronManagerCanAddAnEntry()
    {
        $entry = new stdClass();
        $cronman = new CronManager();
        $cronman->addEntry($entry);
        $this->assertCount(1, $cronman);
    }
}
Exercise 1.0
• Checkout branch ex-1.0
• Create a test for the Entry class
Something like this…
<?php
namespace In2itTestWorkshopCtsr;
use In2itWorkshopCtsrCronManagerEntry;
class EntryTest extends PHPUnit_Framework_TestCase
{
    public function testEntryContainsAllFields() { /** ... */ }
    public function testEntryCanSetEntryElements() { /** ... */ }
}
Something like this… (2)
public function testEntryContainsAllFields()
{
    $entry = new Entry();
    $this->assertCount(0, $entry->getMinutes());
    $this->assertCount(0, $entry->getHours());
    $this->assertCount(0, $entry->getDom());
    $this->assertCount(0, $entry->getMonths());
    $this->assertCount(0, $entry->getDow());
    $this->assertSame('', $entry->getCommand());
}
Something like this… (3)
public function testEntryCanSetEntryElements()
{
    $assetCollection = $this->getMock(
        'In2itWorkshopCtsrCronManagerAssetCollection'
    );
    $entry = new Entry();
    $entry->setMinutes($assetCollection);
    $this->assertInstanceOf(
        'In2itWorkshopCtsrCronManagerAssetCollection', 
        $entry->getMinutes()
    );
    /* 
     * Similar routines for Hours, Days of the Month, Months and Days of the week
     */
    $command = $this->getMock('In2itWorkshopCtsrCronManagerCommand');
    $entry->setCommand($command);
    $this->assertInstanceOf(
        'In2itWorkshopCtsrCronManagerCommand', 
        $entry->getCommand()
    );
}
Overview of classes (ex-1.0)
ctsr-workshop/
src/
ex-1.0/
CronManager.php
tests/
ex-1.0/
CronManager/
EntryTest.php
CronManagerTest.php
Exercise 1.1
• Checkout branch ex-1.1
• Have a look at all the tests
Pop-quiz
// Why are we using Mock objects to test functionality?
$assetCollection = $this->getMock(
    'In2itWorkshopCtsrCronManagerAssetCollection'
);
$asset = $this->getMock(
    'In2itWorkshopCtsrCronManagerAsset'
);
$entry = $this->getMock(
    'In2itWorkshopCtsrCronManagerEntry'
);
Question
• Are we protected against bad input?
• Yes
• No
Create some bad data
public function badDataProvider()
{
    return array (
        array ('foo'),
        array (new stdClass()),
        array (array ()),
        array (1.50),
    );
}
And let’s test it!
/**
 * @dataProvider badDataProvider
 * @covers In2itWorkshopCtsrCronManagerAsset::__construct
 * @covers In2itWorkshopCtsrCronManagerAsset::setValue
 * @covers In2itWorkshopCtsrCronManagerAsset::getValue
 * @expectedException InvalidArgumentException
 */
public function testRejectBadData($badData)
{
    $asset = new Asset($badData);
    $this->fail('Expected InvalidArgumentException to be thrown');
}
Let’s fix that!
/**
 * @param int $value
 * @throws InvalidArgumentException
 */
public function setValue($value)
{
    if (!is_int($value)) {
        throw new InvalidArgumentException(
            'You've provided an invalid argument'
        );
    }
    if (false === ($result = filter_var($value, FILTER_VALIDATE_INT))) {
        throw new InvalidArgumentException(
            'You've provided an invalid argument'
        );
    }
    $this->value = (int) $value;
}
Complete code in
branch ex-1.2
Introduction
Mise en place (preparation)
Running Tests
Starting a new project with TDD
Testing & Improving legacy code
Other tools
Recap & Closing remarks
https://www.flickr.com/photos/ryantylersmith/14010104872
https://www.flickr.com/photos/cmdrcord/9645186380
Legacy code
• Code that was already written
• Not (always) adhering to best
practices
• Not (always) testable
• What developers hate working
on
https://www.flickr.com/photos/archer10/7845300746
<?php
class ModuleManager
{
    public static $modules_install = array();
    /**
     * Includes file with module installation class.
     *
     * Do not use directly.
     *
     * @param string $module_class_name module class name - underscore separated
     * @return bool
     */
    public static final function include_install($module_class_name) {
        if(isset(self::$modules_install[$module_class_name])) return true;
        $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
        if (!file_exists($full_path)) return false;
        ob_start();
        $ret = require_once($full_path);
        ob_end_clean();
        $x = $module_class_name.'Install';
        if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
            trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
        self::$modules_install[$module_class_name] = new $x($module_class_name);
        return true;
    }
}
<?php
include_once __DIR__ . '/../../src/ex-2.0/ModuleManager.php';
class ModuleManagerTest extends PHPUnit_Framework_TestCase
{
    /**
     * @covers ModuleManager::include_install
     */
    public function testModuleManagerCanLoadMailModule()
    {
        $result = ModuleManager::include_install('Mail');
        $this->assertTrue($result);
    }
}
<?php
class ModuleManager
{
    public static $modules_install = array();
    /**
     * Includes file with module installation class.
     *
     * Do not use directly.
     *
     * @param string $module_class_name module class name - underscore separated
     * @return bool
     */
    public static final function include_install($module_class_name) {
        if(isset(self::$modules_install[$module_class_name])) return true;
        $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
        if (!file_exists($full_path)) return false;
        ob_start();
        $ret = require_once($full_path);
        ob_end_clean();
        $x = $module_class_name.'Install';
        if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
            trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
        self::$modules_install[$module_class_name] = new $x($module_class_name);
        return true;
    }
}
<?php
class ModuleManager
{
    public static $modules_install = array();
    /**
     * Includes file with module installation class.
     *
     * Do not use directly.
     *
     * @param string $module_class_name module class name - underscore separated
     * @return bool
     */
    public static final function include_install($module_class_name) {
        if(isset(self::$modules_install[$module_class_name])) return true;
        $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
        if (!file_exists($full_path)) return false;
        ob_start();
        $ret = require_once($full_path);
        ob_end_clean();
        $x = $module_class_name.'Install';
        if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
            trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
        self::$modules_install[$module_class_name] = new $x($module_class_name);
        return true;
    }
}
<?php
class ModuleManager
{
    public static $modules_install = array();
    /**
     * Includes file with module installation class.
     *
     * Do not use directly.
     *
     * @param string $module_class_name module class name - underscore separated
     * @return bool
     */
    public static final function include_install($module_class_name) {
        if(isset(self::$modules_install[$module_class_name])) return true;
        $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
        if (!file_exists($full_path)) return false;
        ob_start();
        $ret = require_once($full_path);
        ob_end_clean();
        $x = $module_class_name.'Install';
        if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
            trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
        self::$modules_install[$module_class_name] = new $x($module_class_name);
        return true;
    }
}
<?php
class ModuleManager
{
    public static $modules_install = array();
    /**
     * Includes file with module installation class.
     *
     * Do not use directly.
     *
     * @param string $module_class_name module class name - underscore separated
     * @return bool
     */
    public static final function include_install($module_class_name) {
        if(isset(self::$modules_install[$module_class_name])) return true;
        $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
        if (!file_exists($full_path)) return false;
        ob_start();
        $ret = require_once($full_path);
        ob_end_clean();
        $x = $module_class_name.'Install';
        if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
            trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
        self::$modules_install[$module_class_name] = new $x($module_class_name);
        return true;
    }
}
<?php
class ModuleManager
{
    public static $modules_install = array();
    /**
     * Includes file with module installation class.
     *
     * Do not use directly.
     *
     * @param string $module_class_name module class name - underscore separated
     * @return bool
     */
    public static final function include_install($module_class_name) {
        if(isset(self::$modules_install[$module_class_name])) return true;
        $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
        if (!file_exists($full_path)) return false;
        ob_start();
        $ret = require_once($full_path);
        ob_end_clean();
        $x = $module_class_name.'Install';
        if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
            trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
        self::$modules_install[$module_class_name] = new $x($module_class_name);
        return true;
    }
}
<?php
include_once __DIR__ . '/../../src/ex-2.0/ModuleManager.php';
class ModuleManagerTest extends PHPUnit_Framework_TestCase
{
    /**
     * @covers ModuleManager::include_install
     */
//    public function testModuleManagerCanLoadMailModule()
//    {
//        $result = ModuleManager::include_install('Mail');
//        $this->assertTrue($result);
//    }
}
https://www.flickr.com/photos/marcgbx/7803086292
/**
 * @covers ModuleManager::include_install
 */
public function testReturnImmediatelyWhenModuleAlreadyLoaded()
{
    $module = 'Foo_Bar';
    ModuleManager::$modules_install[$module] = 1;
    $result = ModuleManager::include_install($module);
    $this->assertTrue($result);
    $this->assertCount(1, ModuleManager::$modules_install);
}
https://www.flickr.com/photos/christian_johannesen/2248244786
/**
 * @covers ModuleManager::include_install
 */
public function testReturnWhenModuleIsNotFound()
{
    $module = 'Foo_Bar';
    $result = ModuleManager::include_install($module);
    $this->assertFalse($result);
    $this->assertEmpty(ModuleManager::$modules_install);
}
public static final function include_install($module_class_name) {
    if(isset(self::$modules_install[$module_class_name])) return true;
    $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
    if (!file_exists($full_path)) return false;
    ob_start();
    $ret = require_once($full_path);
    ob_end_clean();
    $x = $module_class_name.'Install';
    if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
        trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
    self::$modules_install[$module_class_name] = new $x($module_class_name);
    return true;
}
self::$modules_install[$module_class_name]
protected function tearDown()
{
    ModuleManager::$modules_install = array ();
}
https://www.flickr.com/photos/evaekeblad/14780090550
/**
 * @covers ModuleManager::include_install
 * @expectedException PHPUnit_Framework_Error
 */
public function testTriggerErrorWhenInstallClassDoesNotExists()
{
    $module = 'EssClient';
    $result = ModuleManager::include_install($module);
    $this->fail('Expecting loading module EssClient would trigger and error');
}
public static final function include_install($module_class_name) {
    if(isset(self::$modules_install[$module_class_name])) return true;
    $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
    if (!file_exists($full_path)) return false;
    ob_start();
    $ret = require_once($full_path);
    ob_end_clean();
    $x = $module_class_name.'Install';
    if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
        trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
    self::$modules_install[$module_class_name] = new $x($module_class_name);
    return true;
}
public static final function include_install($module_class_name) {
    if(isset(self::$modules_install[$module_class_name])) return true;
    $full_path = __DIR__ . '/modules/' . $module_class_name . '/' . $module_class_name . 'Install.php';
    if (!file_exists($full_path)) return false;
    ob_start();
    $ret = require_once($full_path);
    ob_end_clean();
    $x = $module_class_name.'Install';
    if(!(class_exists($x, false)) || !array_key_exists('ModuleInstall',class_parents($x)))
        trigger_error('Module ' . $module_class_name . ': Invalid install file', E_USER_ERROR);
    self::$modules_install[$module_class_name] = new $x($module_class_name);
    return true;
}
if (!file_exists($full_path)) return false;
Current Filestructure
|-- ModuleManager.php
`-- modules
    |-- EssClient
    |   `-- EssClient.php
    |-- IClient
    |   `-- IClientInstall.php
    `-- Mail
        `-- MailInstall.php
https://www.flickr.com/photos/sis/2497912343
https://www.flickr.com/photos/fragiletender/5332586299
Current Filestructure
|-- ModuleManager.php
`-- modules
    |-- EssClient
    |   `-- EssClient.php
    |-- IClient
    |   `-- IClientInstall.php
    `-- Mail
        `-- MailInstall.php
/**
 * @covers ModuleManager::include_install
 * @expectedException PHPUnit_Framework_Error
 */
public function testTriggerErrorWhenInstallClassDoesNotExists()
{
    $module = 'IClient';
    $result = ModuleManager::include_install($module);
    $this->fail('Expecting loading module EssClient would trigger and error');
}
/**
 * @covers ModuleManager::include_install
 */
public function testModuleManagerCanLoadMailModule()
{
    $result = ModuleManager::include_install('Mail');
    $this->assertTrue($result);
}
Get the code
branch ex-2.0
https://www.flickr.com/photos/ahhyeah/454494396
What to do
• Your legacy code has no return values?
    /**
     * Process Bank Payment files
     */
    public function processBankPayments()
    {
        $this->getLogger()->log('Starting bank payment process', Zend_Log::INFO);
        foreach ($this->_getBankFiles() as $bankFile) {
            $bankData = $this->_processBankFile($bankFile);
            $this->getLogger()->log('Processing ' . $bankData->transactionId,
                Zend_Log::DEBUG
            );
            /** @var Contact_Model_Contact $contact */
            $contact = $this->getMapper('Contact_Model_Mapper_Contact')
                ->findContactByBankAccount($bankData->transactionAccount);
            if (null !== $contact) {
                $this->getLogger()->log(sprintf(
                    'Found contact "%s" for bank account %s',
                    $contact->getName(),
                    $bankData->transactionAccount
                ), Zend_Log::DEBUG);
                $data = array (
                    'amount' => $bankData->transactionAmount,
                    'payment_date' => $bankData->transactionDate
                );
                $this->getMapper('Invoice_Model_Mapper_Payments')
                    ->updatePayment($data,
                        array ('contact_id = ?' => $contact->getContactId())
                    );
                $this->_moveBankFile($bankFile,
                    $this->getPath() . DIRECTORY_SEPARATOR . self::PROCESS_SUCCEEDED
                );
            } else {
                $this->getLogger()->log(sprintf(
                    'Could not match bankaccount "%s" with a contact',
                    $bankData->transactionAccount
                ), Zend_Log::WARN);
                $this->_moveBankFile($bankFile,
                    $this->getPath() . DIRECTORY_SEPARATOR . self::PROCESS_FAILED
                );
            }
        }
    }
    public function testProcessingBankPayments()
    {
        $contact = $this->getMock(
            'Contact_Model_Contact',
            array ('getContactId', 'getName')
        );
        $contact->expects($this->any())
            ->method('getContactId')
            ->will($this->returnValue(1));
        $contact->expects($this->any())
            ->method('getName')
            ->will($this->returnValue('Foo Bar'));
        $contactMapper = $this->getMock('Contact_Model_Mapper_Contact',
            array ('findContactByBankAccount')
        );
        $contactMapper->expects($this->any())
            ->method('findContactByBankAccount')
            ->will($this->returnValue($contact));
        $paymentsMapper = $this->getMock('Invoice_Model_Mapper_Payments',
            array ('updatePayment')
        );
        $logMock = new Zend_Log_Writer_Mock();
        $logger = new Zend_Log();
        $logger->setWriter($logMock);
        $logger->setPriority(Zend_Log::DEBUG);
        $as400 = new Payments_Service_As400();
        $as400->addMapper($contactMapper, 'Contact_Model_Mapper_Contact')
            ->addMapper($paymentsMapper, 'Invoice_Model_Mapper_Payments')
            ->setPath(__DIR__ . DIRECTORY_SEPARATOR . '_files')
            ->setLogger($logger);
        $as400->processBankPayments();
        $this->assertCount(3, $logMock->events);
        $this->assertEquals('Processing 401341345', $logMock->events[1]);
        $this->assertEquals(
            'Found contact "Foo Bar" for bank account BE93522511513933',
            $logMock->events[2]
        );
    }
        $as400 = new Payments_Service_As400();
        $as400->addMapper($contactMapper, 'Contact_Model_Mapper_Contact')
            ->addMapper($paymentsMapper, 'Invoice_Model_Mapper_Payments')
            ->setPath(__DIR__ . DIRECTORY_SEPARATOR . '_files')
            ->setLogger($logger);
        $as400->processBankPayments();
        $this->assertCount(3, $logMock->events);
        $this->assertEquals('Processing 401341345', $logMock->events[1]);
        $this->assertEquals(
            'Found contact "Foo Bar" for bank account BE93522511513933',
            $logMock->events[2]
        );
Get the code
branch ex-2.1
https://www.flickr.com/photos/ahhyeah/454494396
Privates exposed
http://www.slashgear.com/former-tsa-agent-admits-we-knew-full-body-scanners-didnt-work-31315288/
Dependency
• __construct
• get_module_name
• get_version_min
• get_version_max
• is_satisfied_by
• requires
• requires_exact
• requires_at_least
• requires_range
A private constructor!
<?php
defined("_VALID_ACCESS") || die('Direct access forbidden');
/**
 * This class provides dependency requirements
 * @package epesi-base
 * @subpackage module 
 */
class Dependency {
    private $module_name;
    private $version_min;
    private $version_max;
    private $compare_max;
    private function __construct(
$module_name, $version_min, $version_max, $version_max_is_ok = true) {
        $this->module_name = $module_name;
        $this->version_min = $version_min;
        $this->version_max = $version_max;
        $this->compare_max = $version_max_is_ok ? '<=' : '<';
    }
    /** ... */
}
Don’t touch my junk!
https://www.flickr.com/photos/caseymultimedia/5412293730
House of Reflection
https://www.flickr.com/photos/tabor-roeder/8250770115
Let’s do this…
<?php
require_once 'include.php';
class DependencyTest extends PHPUnit_Framework_TestCase
{
    public function testConstructorSetsProperSettings()
    {
        require_once 'include/module_dependency.php';
        // We have a problem, the constructor is private!
    }
}
Let’s use the static
$params = array (
    'moduleName' => 'Foo_Bar',
    'minVersion' => 0,
    'maxVersion' => 1,
    'maxOk' => true,
);
// We use a static method for this test
$dependency = Dependency::requires_range(
    $params['moduleName'],
    $params['minVersion'],
    $params['maxVersion'],
    $params['maxOk']
);
// We use reflection to see if properties are set correctly
$reflectionClass = new ReflectionClass('Dependency');
Use the reflection to assert
// Let's retrieve the private properties
$moduleName = $reflectionClass->getProperty('module_name');
$moduleName->setAccessible(true);
$minVersion = $reflectionClass->getProperty('version_min');
$minVersion->setAccessible(true);
$maxVersion = $reflectionClass->getProperty('version_max');
$maxVersion->setAccessible(true);
$maxOk = $reflectionClass->getProperty('compare_max');
$maxOk->setAccessible(true);
// Let's assert
$this->assertEquals($params['moduleName'], $moduleName->getValue($dependency),
    'Expected value does not match the value set’);
$this->assertEquals($params['minVersion'], $minVersion->getValue($dependency),
    'Expected value does not match the value set’);
$this->assertEquals($params['maxVersion'], $maxVersion->getValue($dependency),
    'Expected value does not match the value set’);
$this->assertEquals('<=', $maxOk->getValue($dependency),
    'Expected value does not match the value set');
Run tests
Code Coverage
Introduction
Mise en place (preparation)
Running Tests
Starting a new project with TDD
Testing & Improving legacy code
Other tools
Recap & Closing remarks
https://www.flickr.com/photos/ryantylersmith/14010104872
https://www.flickr.com/photos/florianric/7263382550
SCM is a must!
FTP is not a SCM
Use Composer
Automation tools
https://www.gnu.org/graphics/heckert_gnu.small.png
Common CI systems
Online CI systems
Online CI systems
Online CI systems
Some other test tools
https://www.gnu.org/graphics/heckert_gnu.small.png
Introduction
Mise en place (preparation)
Running Tests
Starting a new project with TDD
Testing & Improving legacy code
Other tools
Recap & Closing remarks
https://www.flickr.com/photos/ryantylersmith/14010104872
https://www.flickr.com/photos/didmyself/8030013349
https://www.flickr.com/photos/wjserson/3310851114
https://www.flickr.com/photos/joeshlabotnik/2384495536
https://www.flickr.com/photos/much0/8552353901
https://www.flickr.com/photos/thomashawk/10490113913
References
in it2PROFESSIONAL PHP SERVICES
Michelangelo van Dam
Zend Certified Engineer
training@in2it.be - www.in2it.be - T in2itvof - F in2itvof
PHPUnit
Getting Started
Advanced Testing
Zend Framework 2
Fundamentals
Advanced
Azure PHP
Quick time to market
Scale up and out
jQuery
Professional jQuery
PHP
PHP for beginners
Professional PHP
HTML & CSS
The Basics
Our training courses
https://www.flickr.com/photos/drewm/3191872515

Create, test, secure, repeat