Your code are my tests

Post on 14-Jul-2015

689 views 2 download

Tags:

Transcript of Your code are my tests

Your code are my tests

How to test legacy code

in it2PROFESSIONAL PHP SERVICES

ADVISORYIN 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

http

s://w

ww.

flick

r.com

/pho

tos/

akra

bat/8

7843

1881

3

Using Social Media?Tag it #mytests

http

://w

ww.

flick

r.com

/pho

tos/

andy

ofne

/463

3356

197

http

://w

ww.

flick

r.com

/pho

tos/

andy

ofne

/463

3356

197

Why bother with testing?

http

s://w

ww.

flick

r.com

/pho

tos/

vial

bost

/553

3266

530

Most common excuses why developers don’t test

• no time

• no budget

• deliver tests after finish project (never)

• devs don’t know how

http

s://w

ww.

flick

r.com

/pho

tos/

dasp

rid/8

1479

8630

7

No excuses!!!

Crea%ve  Co

mmon

s  -­‐  h.p://www.flickr.com

/pho

tos/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

http

s://w

ww.

flick

r.com

/pho

tos/

bobj

agen

dorf/

8535

3168

36

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

HackersBAD DATA

Web ServicesStupid users

OWASP top 10 exploits

https://www.owasp.org/index.php/Top_10_2013-Top_10

Filtering & Validation

Smallest unit of code

http

s://w

ww.

flick

r.com

/pho

tos/

tool

stop

/454

6017

269

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  Myapp\Common\Payment;      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!

http

s://w

ww.

flick

r.com

/pho

tos/

bertk

not/8

1752

1490

9

Real code, real apps

github.com/Telaxus/EPESI

Running the project

Where are the TESTS?

Where are the TESTS?

Oh noes, no tests!

http

s://w

ww.

flick

r.com

/pho

tos/

mjh

agen

/297

3212

926

Let’s get started

http

s://w

ww.

flick

r.com

/pho

tos/

npob

re/2

6015

8225

6

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_install

/**  * 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

http

s://w

ww.

flick

r.com

/pho

tos/

dazj

ohns

on/7

7208

0682

4

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

http

s://w

ww.

flick

r.com

/pho

tos/

hugo

90/6

9807

1264

3

Privates exposed

http

://w

ww.

slas

hgea

r.com

/form

er-ts

a-ag

ent-a

dmits

-we-

knew

-full-

body

-sca

nner

s-di

dnt-w

ork-

3131

5288

/

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!

http

s://w

ww.

flick

r.com

/pho

tos/

case

ymul

timed

ia/5

4122

9373

0

House of Reflection

http

s://w

ww.

flick

r.com

/pho

tos/

tabo

r-roe

der/8

2507

7011

5

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

http

s://w

ww.

flick

r.com

/pho

tos/

rnug

raha

/200

3147

365

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

http

s://w

ww.

flick

r.com

/pho

tos/

lwr/1

3442

5422

35

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 youHave a great conference

http

://w

ww.

flick

r.com

/pho

tos/

drew

m/3

1918

7251

5