Your code are my tests
-
Upload
michelangelo-van-dam -
Category
Engineering
-
view
689 -
download
2
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 [email protected].
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
Recommended reading
http
s://w
ww.
flick
r.com
/pho
tos/
lwr/1
3442
5422
35
Contact us
in it2PROFESSIONAL PHP SERVICES
Michelangelo van Dam [email protected]
www.in2it.be
PHP Consulting - Training - QA
Thank youHave a great conference
http
://w
ww.
flick
r.com
/pho
tos/
drew
m/3
1918
7251
5