Your code
are my tests
How to test legacy code
in it2PROFESSIONAL PHP SERVICES
ADVISORY
IN ORDER TO EXPLAIN CERTAIN SITUATIONS YOU MIGHT FACE IN YOUR
DEVELOPMENT CAREER, WE WILL BE DISCUSSING THE USAGE OF
PRIVATES AND PUBLIC EXPOSURE. IF THESE TOPICS OFFEND OR
UPSET YOU, WE WOULD LIKE TO ASK YOU TO LEAVE THIS ROOM NOW.
THE SPEAKER NOR THE ORGANISATION CANNOT BE HELD
ACCOUNTABLE FOR MENTAL DISTRESS OR ANY FORMS OF DAMAGE
YOU MIGHT ENDURE DURING OR AFTER THIS PRESENTATION. FOR
COMPLAINTS PLEASE INFORM ORGANISATION AT INFO@IN2IT.BE.
Michelangelo van Dam
PHP Consultant
Community Leader
President of PHPBenelux
Contributor to PHP projects
T @DragonBe | F DragonBe
https://www.flickr.com/photos/akrabat/8784318813
Using Social Media?
Tag it #mytests
http://www.flickr.com/photos/andyofne/4633356197
http://www.flickr.com/photos/andyofne/4633356197
Why bother with testing?
https://www.flickr.com/photos/vialbost/5533266530
Most common excuses
why developers don’t test
• no time
• no budget
• deliver tests after finish project
(never)
• devs don’t know how
https://www.flickr.com/photos/dasprid/8147986307
No excuses!!!
Crea%ve	
  Commons	
  -­‐	
  h.p://www.flickr.com/photos/akrabat/8421560178
Responsibility issue
• As a developer, it’s your job to
• write code & fixing bugs
• add documentation
• write & update unit tests
Pizza principleTopping:	
  your	
  tests
Box:	
  your	
  documenta%on
Dough:	
  your	
  code
Benefits of testing
• Direct feedback (test fails)
• Once a test is made, it will always be tested
• Easy to refactor existing code (protection)
• Easy to debug: write a test to see if a bug is
genuine
• Higher confidence and less uncertainty
Rule of thumb
“Whenever you are tempted to type something into a
print statement or a debugger expression, write it as
a test instead.”
— Source: Martin Fowler
Warming up
https://www.flickr.com/photos/bobjagendorf/8535316836
PHPUnit
• PHPUnit is a port of xUnit testing framework
• Created by “Sebastian Bergmann”
• Uses “assertions” to verify behaviour of “unit of code”
• Open source and hosted on GitHub
• See https://github.com/sebastianbergmann/phpunit
• Can be installed using:
• PEAR
• PHAR
• Composer
Approach for testing
• Instantiate a “unit-of-code”
• Assert expected result against actual result
• Provide a custom error message
Available assertions
• assertArrayHasKey()
• 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()
• assertSelectCount()
• assertSelectEquals()
• assertSelectRegExp()
• assertStringEndsWith()
• assertStringEqualsFile()
• assertStringStartsWith()
• assertTag()
• assertThat()
• assertTrue()
• assertXmlFileEqualsXmlFile()
• assertXmlStringEqualsXmlFile()
• assertXmlStringEqualsXmlString()
To	
  protect	
  and	
  to	
  serve
Data is tainted, ALWAYS
Hackers
BAD DATA
Web Services
Stupid users
OWASP top 10 exploits
https://www.owasp.org/index.php/Top_10_2013-Top_10
Filtering & Validation
Smallest unit of code
https://www.flickr.com/photos/toolstop/4546017269
Example class
<?php
/**
 * Example class
 */
class MyClass
{
    /** ... */
    public function doSomething($requiredParam, $optionalParam = null)
    {
        if (!filter_var(
            $requiredParam, FILTER_SANITIZE_STRING, FILTER_FLAG_ENCODE_HIGH
        )) {
            throw new InvalidArgumentException('Invalid argument provided');
        }
        if (null !== $optionalParam) {
            if (!filter_var(
                $optionalParam, FILTER_SANITIZE_STRING, FILTER_FLAG_ENCODE_HIGH
            )) {
                throw new InvalidArgumentException('Invalid argument provided');
            }
            $requiredParam .= ' - ' . $optionalParam;
        }
        return $requiredParam;
    }
}
Testing for good
   /** ... */
    public function testClassAcceptsValidRequiredArgument()
    {
        $expected = $argument = 'Testing PHP Class';
        $myClass = new MyClass;
        $result = $myClass->doSomething($argument);
        $this->assertSame($expected, $result, 
            'Expected result differs from actual result');
    }
   /** ... */    
    public function testClassAcceptsValidOptionalArgument()
    {
        $requiredArgument = 'Testing PHP Class';
        $optionalArgument = 'Is this not fun?!?';
        $expected = $requiredArgument . ' - ' . $optionalArgument;
        $myClass = new MyClass;
        $result = $myClass->doSomething($requiredArgument, $optionalArgument);
        $this->assertSame($expected, $result, 
            'Expected result differs from actual result');
    }
Testing for bad
    /**
     * @expectedException InvalidArgumentException
     */
    public function testExceptionIsThrownForInvalidRequiredArgument()
    {
        $expected = $argument = new StdClass;
        $myClass = new MyClass;
        $result = $myClass->doSomething($argument);
        $this->assertSame($expected, $result, 
            'Expected result differs from actual result');
    }
    
    /**
     * @expectedException InvalidArgumentException
     */
    public function testExceptionIsThrownForInvalidOptionalArgument()
    {
        $requiredArgument = 'Testing PHP Class';
        $optionalArgument = new StdClass;
        $myClass = new MyClass;
        $result = $myClass->doSomething($requiredArgument, $optionalArgument);
        $this->assertSame($expected, $result, 
            'Expected result differs from actual result');
    }
Example: testing payments
<?php	
  
namespace	
  MyappCommonPayment;	
  
	
  	
  
class	
  ProcessTest	
  extends	
  PHPUnit_Framework_TestCase	
  
{	
  
	
  	
  	
  	
  public	
  function	
  testPaymentIsProcessedCorrectly()	
  
	
  	
  	
  	
  {	
  
	
  	
  	
  	
  	
  	
  	
  	
  $customer	
  =	
  new	
  Customer(/*	
  data	
  for	
  customer	
  */);	
  
	
  	
  	
  	
  	
  	
  	
  	
  $transaction	
  =	
  new	
  Transaction(/*	
  data	
  for	
  transaction	
  */);	
  
	
  	
  	
  	
  	
  	
  	
  	
  $process	
  =	
  new	
  Process('sale',	
  $customer,	
  $transaction);	
  
	
  	
  	
  	
  	
  	
  	
  	
  $process-­‐>pay();	
  
	
  	
  
	
  	
  	
  	
  	
  	
  	
  	
  $this-­‐>assertTrue($process-­‐>paymentApproved());	
  
	
  	
  	
  	
  	
  	
  	
  	
  $this-­‐>assertEquals('PAY-­‐17S8410768582940NKEE66EQ',	
  $process-­‐
>getPaymentId());	
  
	
  	
  	
  	
  }	
  
}
We don’t live in a fairy tale!
https://www.flickr.com/photos/bertknot/8175214909
Real code, real apps
github.com/Telaxus/EPESI
Running the project
Where are the TESTS?
Where are the TESTS?
Oh noes, no tests!
https://www.flickr.com/photos/mjhagen/2973212926
Let’s get started
https://www.flickr.com/photos/npobre/2601582256
How to get about it?
Setting up for testing
<phpunit colors="true" stopOnError="true" stopOnFailure="true">
<testsuites>
<testsuite name="EPESI admin tests">
<directory phpVersion="5.3.0">tests/admin</directory>
</testsuite>
<testsuite name="EPESI include tests">
<directory phpVersion="5.3.0">tests/include</directory>
</testsuite>
<testsuite name="EPESI modules testsuite">
<directory phpVersion="5.3.0">tests/modules</directory>
</testsuite>
</testsuites>
<php>
<const name="DEBUG_AUTOLOADS" value="1"/>
<const name="CID" value="1234567890123456789"/>
</php>
<logging>
<log type="coverage-html" target="build/coverage" charset="UTF-8"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
<log type="junit" target="build/logs/junit.xml"/>
</logging>
</phpunit>
ModuleManager
• not_loaded_modules
• loaded_modules
• modules
• modules_install
• modules_common
• root
• processing
• processed_modules
• include_install
• include_common
• include_main
• create_load_priority_array
• check_dependencies
• satisfy_dependencies
• get_module_dir_path
• get_module_file_name
• list_modules
• exists
• register
• unregister
• is_installed
• upgrade
• downgrade
• get_module_class_name
• install
• uninstall
• get_processed_modules
• get_load_priority_array
• new_instance
• get_instance
• create_data_dir
• remove_data_dir
• get_data_dir
• load_modules
• create_common_cache
• create_root
• check_access
• call_common_methods
• check_common_methods
• required_modules
• reset_cron
ModuleManager::module_in
stall
/**
 * Includes file with module installation class.
 *
 * Do not use directly.
 *
 * @param string $module_class_name module class name - underscore separated
 */
public static final function include_install($module_class_name) {
    if(isset(self::$modules_install[$module_class_name])) return true;
    $path = self::get_module_dir_path($module_class_name);
    $file = self::get_module_file_name($module_class_name);
    $full_path = 'modules/' . $path . '/' . $file . '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 '.$path.': Invalid install file',E_USER_ERROR);
    self::$modules_install[$module_class_name] = new $x($module_class_name);
    return true;
}
Testing first condition
<?php
require_once 'include.php';
class ModuleManagerTest extends PHPUnit_Framework_TestCase
{
    protected function tearDown()
    {
        ModuleManager::$modules_install = array ();
    }
    public function testReturnImmediatelyWhenModuleAlreadyLoaded()
    {
        $module = 'Foo_Bar';
        ModuleManager::$modules_install[$module] = 1;
        $result = ModuleManager::include_install($module);
        $this->assertTrue($result,
            'Expecting that an already installed module returns true');
        $this->assertCount(1, ModuleManager::$modules_install,
            'Expecting to find 1 module ready for installation');
    }
}
Run test
Check coverage
Test for second condition
public function testLoadingNonExistingModuleIsNotExecuted()
{
    $module = 'Foo_Bar';
    $result = ModuleManager::include_install($module);
    $this-
>assertFalse($result, 'Expecting failure for loading Foo_Bar');
    $this->assertEmpty(ModuleManager::$modules_install,
        'Expecting to find no modules ready for installation');
}
Run tests
Check coverage
Test for third condition
public function testNoInstallationOfModuleWithoutInstallationClass(
)
{
    $module = 'EssClient_IClient';
    $result = ModuleManager::include_install($module);
    $this-
>assertFalse($result, 'Expecting failure for loading Foo_Bar');
    $this->assertEmpty(ModuleManager::$modules_install,
        'Expecting to find no modules ready for installation');
}
Run tests
Check code coverage
Non-executable code
https://www.flickr.com/photos/dazjohnson/7720806824
Test for success
public function testIncludeClassFileForLoadingModule()
{
    $module = 'Base_About';
    $result = ModuleManager::include_install($module);
    $this->assertTrue($result, 'Expected module to be loaded');
    $this->assertCount(1, ModuleManager::$modules_install,
        'Expecting to find 1 module ready for installation');
}
Run tests
Check code coverage
Look at the global coverage
Bridging gaps
https://www.flickr.com/photos/hugo90/6980712643
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
Yes, paradise exists
https://www.flickr.com/photos/rnugraha/2003147365
Unit testing is not
difficult!
Get started
PHP has all the tools
And there are more
roads to Rome
You’re one-stop fix
phpunit.de
Recommended reading
https://www.flickr.com/photos/lwr/13442542235
Contact us
in it2PROFESSIONAL PHP SERVICES
Michelangelo van Dam
michelangelo@in2it.be
www.in2it.be
PHP Consulting - Training - QA
cfp.phpbenelux.eu
Submit your proposals before
October 14, 2015
Thank you
Have a great conference
http://www.flickr.com/photos/drewm/3191872515

Your code are my tests