Download - Your code are my tests - ZendUncon 2014
2
Your code are my testsHow to test legacy code
2
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 [email protected].
3
Michelangelo van Dam!!
PHP Consultant Community Leader
President of PHPBenelux Contributor
Why bother with tes3ng?
4
http
s://w
ww.
flick
r.com
/pho
tos/
vial
bost
/553
3266
530
Reasons why not to test
• No 0me • No budget • We deliver tests a8er delivery ( this means never ) • We don’t know how…
5
No excuses!!!
6
Crea0ve Co
mmon
s -‐ hBp://www.flickr.com
/pho
tos/akrabat/8421560178
Responsibility issue
• As a developer, it’s your job to • write code & fixing bugs • add documenta0on • write & update unit tests
7
Pizza principle
8
Topping: your tests
Box: your documenta0on
Dough: your code
Benefits of tes3ng
• Direct feedback (test fails) • Once a test is made, it will always be tested • Easy to refactor exis0ng code (protec0on) • Easy to debug: write a test to see if a bug is genuine • Higher confidence and less uncertainty
9
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: Mar?n Fowler
10
Warming up
11
http
s://w
ww.
flick
r.com
/pho
tos/
bobj
agen
dorf/
8535
3168
36
PHPUnit
• PHPUnit is a port of xUnit tes0ng framework • Created by “Sebas0an Bergmann” • Uses “asser0ons” to verify behaviour of “unit of code” • Open source and hosted on GitHub
• See hBps://github.com/sebas0anbergmann/phpunit • Can be installed using:
• PEAR • PHAR • Composer
12
Approach for tes3ng
• Instan0ate a “unit-‐of-‐code” • Assert expected result against actual result • Provide a custom error message
13
Available asser3ons• assertArrayHasKey() • assertClassHasABribute() • assertClassHasSta0cABribute() • assertContains() • assertContainsOnly() • assertContainsOnlyInstancesOf() • assertCount() • assertEmpty() • assertEqualXMLStructure() • assertEquals() • assertFalse() • assertFileEquals() • assertFileExists() • assertGreaterThan() • assertGreaterThanOrEqual() • assertInstanceOf() • assertInternalType() • assertJsonFileEqualsJsonFile() • assertJsonStringEqualsJsonFile() • assertJsonStringEqualsJsonString()
• assertLessThan() • assertLessThanOrEqual() • assertNull() • assertObjectHasABribute() • assertRegExp() • assertStringMatchesFormat() • assertStringMatchesFormatFile() • assertSame() • assertSelectCount() • assertSelectEquals() • assertSelectRegExp() • assertStringEndsWith() • assertStringEqualsFile() • assertStringStartsWith() • assertTag() • assertThat() • assertTrue() • assertXmlFileEqualsXmlFile() • assertXmlStringEqualsXmlFile() • assertXmlStringEqualsXmlString()
14
To protect and to serve
15
Data is tainted, ALWAYS
16
Hackers BAD DATA
Web S
ervicesStupid users
OWASP top 10 exploits
19https://www.owasp.org/index.php/Top_10_2013-Top_10
Smallest unit of code
21
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; ! } !}
22
Tes3ng 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'); ! }
23
Tes3ng 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'); ! }
24
Example: tes3ng 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()); } }
25
We don’t live in a fairy tale!
26
http
s://w
ww.
flick
r.com
/pho
tos/
bertk
not/8
1752
1490
9
Real code, real apps
27
github.com/Telaxus/EPESI
28
Running the project
29
Where are the TESTS?
30
Where are the TESTS?
31
Oh noes, no tests!
32
http
s://w
ww.
flick
r.com
/pho
tos/
mjh
agen
/297
3212
926
Let’s get started
33
http
s://w
ww.
flick
r.com
/pho
tos/
npob
re/2
6015
8225
6
How to get about it?
34
Se]ng up for tes3ng<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>
35
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 • sa0sfy_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
36
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; !}
37
Tes3ng first condi3on<?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'); ! } !}
38
Run test
39
Check coverage
40
Test for second condi3onpublic 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'); !}
41
Run tests
42
Check coverage
43
Test for third condi3onpublic 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'); !}
44
Run tests
45
Check code coverage
46
Non-‐executable code
47
http
s://w
ww.
flick
r.com
/pho
tos/
dazj
ohns
on/7
7208
0682
4
Test for successpublic 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'); !}
48
Run tests
49
Check code coverage
50
Look at the global coverage
51
Bridging gaps
52
http
s://w
ww.
flick
r.com
/pho
tos/
hugo
90/6
9807
1264
3
Privates exposed
53 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_sa0sfied_by • requires • requires_exact • requires_at_least • requires_range
54
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 ? '<=' : '<'; ! } !! /** ... */ !}
55
Don’t touch my junk!
56
http
s://w
ww.
flick
r.com
/pho
tos/
case
ymul
timed
ia/5
4122
9373
0
House of Reflec3on
57
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!! } !}
58
Let’s use the sta3c$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');
59
Use the reflec3on 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');
60
Run tests
61
Code Coverage
62
Yes, paradise exists
63
http
s://w
ww.
flick
r.com
/pho
tos/
rnug
raha
/200
3147
365
Unit tes3ng is not difficult!
64
You just need to get started
65
PHP has all the tools
66
And there are more roads to Rome
67
Recommended reading
68
69
joind.in/12558!!
Slides are on joindin While you're there give some feedback
!If you liked my talk, thanks.
If not, let me know how to improve it
Ques3ons?
70
http
s://w
ww.
flick
r.com
/pho
tos/
mdp
ettit
t/867
1901
426