Advertisement

Create, test, secure, repeat

Senior PHP architect & QA Specialist at In2IT
Jun. 25, 2015
Advertisement

More Related Content

Advertisement
Advertisement

Create, test, secure, repeat

  1. Create, Test, Secure & Repeat Part of the in2it Quality Assurance Training in it2PROFESSIONAL PHP SERVICES
  2. https://www.flickr.com/photos/90371939@N00/4344878104
  3. Michelangelo van Dam PHP Consultant, Community Leader & Trainer https://www.flickr.com/photos/akrabat/8784318813
  4. 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
  5. https://www.flickr.com/photos/eriktorner/8048428729
  6. 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
  7. 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
  8. 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
  9. https://www.flickr.com/photos/greenboy/3895219425
  10. https://www.flickr.com/photos/greenboy/3895219425 https://www.flickr.com/photos/tonivc/
  11. https://www.flickr.com/photos/greenboy/3895219425 https://www.flickr.com/photos/tonivc/ https://www.flickr.com/photos/68751915@N05/6736158045
  12. https://www.flickr.com/photos/akrabat/8421560178
  13. https://www.flickr.com/photos/jakerust/16223669794
  14. A little test
  15. Who created PHPUnit and is now project lead? A. Chris Hartjes B. Sebastian Bergmann C. Stefan Priepsch
  16. 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
  17. 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
  18. Assertion • is a true/false statement • to match an expectation • with the result of functionality under test
  19. 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()
  20. 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()
  21. 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() );
  22. Annotations • Provide automatic features • to execute arbitrary functionality • without having to create logic
  23. 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
  24. @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     } }
  25. 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
  26. @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);     } }
  27. @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');     } }
  28. www.phpunit.de
  29. 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
  30. https://www.flickr.com/photos/427/2253530041
  31. 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
  32. https://www.flickr.com/photos/rueful/5906546599
  33. 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
  34. 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
  35. 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
  36. https://www.flickr.com/photos/rhinoneal/8060238470
  37. phpunit.xml
  38. <?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>
  39. 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
  40. https://www.flickr.com/photos/cgc/6776701
  41. Composer PHPUnit ./vendor/bin/phpunit Phar PHPUnit php phpunit.phar
  42. 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
  43. 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()         );     } }
  44. 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
  45. 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
  46. Write our class <?php namespace In2itWorkshopCtsr; class SampleClass {     public function doSomething()     {         return 'Hello World!';     } }
  47. 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
  48. 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
  49. Modifying the test class     public function testSomethingReturnsArgument()     {         $sampleClass = new SampleClass();         $argument = 'Class';         $this->assertSame(             sprintf('Hello %s!', $argument),             $sampleClass->doSomething($argument)         );     }
  50. Modify the SampleClass <?php namespace In2itWorkshopCtsr; class SampleClass {     public function doSomething($argument)     {         return 'Hello ' . $argument . '!';     } }
  51. Update further <?php namespace In2itWorkshopCtsr; class SampleClass {     public function doSomething($argument = 'World')     {         return 'Hello ' . $argument . '!';     } }
  52. 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
  53. https://www.flickr.com/photos/raster/3563135804
  54. 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
  55. https://www.flickr.com/photos/esoterika/5347998757
  56. Schedule Manager
  57. Functional requirements • Application needs to manage cron entries, where all scheduled tasks are stored in a database and written to the crontab when saved.
  58. 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
  59. 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
  60. 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)
  61. https://www.flickr.com/photos/caveman_92223/3968354387
  62. • 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
  63. <?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);     } }
  64. <?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() {/**... */} }
  65. <?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);     } }
  66. <?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);     } }
  67. Exercise 1.0 • Checkout branch ex-1.0 • Create a test for the Entry class
  68. Something like this… <?php namespace In2itTestWorkshopCtsr; use In2itWorkshopCtsrCronManagerEntry; class EntryTest extends PHPUnit_Framework_TestCase {     public function testEntryContainsAllFields() { /** ... */ }     public function testEntryCanSetEntryElements() { /** ... */ } }
  69. 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()); }
  70. 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()     ); }
  71. Overview of classes (ex-1.0) ctsr-workshop/ src/ ex-1.0/ CronManager.php tests/ ex-1.0/ CronManager/ EntryTest.php CronManagerTest.php
  72. Exercise 1.1 • Checkout branch ex-1.1 • Have a look at all the tests
  73. Pop-quiz // Why are we using Mock objects to test functionality? $assetCollection = $this->getMock(     'In2itWorkshopCtsrCronManagerAssetCollection' ); $asset = $this->getMock(     'In2itWorkshopCtsrCronManagerAsset' ); $entry = $this->getMock(     'In2itWorkshopCtsrCronManagerEntry' );
  74. Question • Are we protected against bad input? • Yes • No
  75. Create some bad data public function badDataProvider() {     return array (         array ('foo'),         array (new stdClass()),         array (array ()),         array (1.50),     ); }
  76. 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'); }
  77. 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; }
  78. Complete code in branch ex-1.2
  79. 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
  80. https://www.flickr.com/photos/cmdrcord/9645186380
  81. 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
  82. <?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;     } }
  83. <?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);     } }
  84. <?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;     } }
  85. <?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;     } }
  86. <?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;     } }
  87. <?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;     } }
  88. <?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;     } }
  89. <?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); //    } }
  90. https://www.flickr.com/photos/marcgbx/7803086292
  91. /**  * @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); }
  92. https://www.flickr.com/photos/christian_johannesen/2248244786
  93. /**  * @covers ModuleManager::include_install  */ public function testReturnWhenModuleIsNotFound() {     $module = 'Foo_Bar';     $result = ModuleManager::include_install($module);     $this->assertFalse($result);     $this->assertEmpty(ModuleManager::$modules_install); }
  94. 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]
  95. protected function tearDown() {     ModuleManager::$modules_install = array (); }
  96. https://www.flickr.com/photos/evaekeblad/14780090550
  97. /**  * @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'); }
  98. 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; }
  99. 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;
  100. Current Filestructure |-- ModuleManager.php `-- modules     |-- EssClient     |   `-- EssClient.php     |-- IClient     |   `-- IClientInstall.php     `-- Mail         `-- MailInstall.php
  101. https://www.flickr.com/photos/sis/2497912343
  102. https://www.flickr.com/photos/fragiletender/5332586299
  103. Current Filestructure |-- ModuleManager.php `-- modules     |-- EssClient     |   `-- EssClient.php     |-- IClient     |   `-- IClientInstall.php     `-- Mail         `-- MailInstall.php
  104. /**  * @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'); }
  105. /**  * @covers ModuleManager::include_install  */ public function testModuleManagerCanLoadMailModule() {     $result = ModuleManager::include_install('Mail');     $this->assertTrue($result); }
  106. Get the code branch ex-2.0 https://www.flickr.com/photos/ahhyeah/454494396
  107. What to do • Your legacy code has no return values?
  108.     /**      * 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                 );             }         }     }
  109.     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]         );     }
  110.         $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]         );
  111. Get the code branch ex-2.1 https://www.flickr.com/photos/ahhyeah/454494396
  112. Privates exposed http://www.slashgear.com/former-tsa-agent-admits-we-knew-full-body-scanners-didnt-work-31315288/
  113. Dependency • __construct • get_module_name • get_version_min • get_version_max • is_satisfied_by • requires • requires_exact • requires_at_least • requires_range
  114. 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 ? '<=' : '<';     }     /** ... */ }
  115. Don’t touch my junk! https://www.flickr.com/photos/caseymultimedia/5412293730
  116. House of Reflection https://www.flickr.com/photos/tabor-roeder/8250770115
  117. 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!     } }
  118. 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');
  119. 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');
  120. Run tests
  121. Code Coverage
  122. 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
  123. https://www.flickr.com/photos/florianric/7263382550
  124. SCM is a must!
  125. FTP is not a SCM
  126. Use Composer
  127. Automation tools https://www.gnu.org/graphics/heckert_gnu.small.png
  128. Common CI systems
  129. Online CI systems
  130. Online CI systems
  131. Online CI systems
  132. Some other test tools https://www.gnu.org/graphics/heckert_gnu.small.png
  133. 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
  134. https://www.flickr.com/photos/didmyself/8030013349
  135. https://www.flickr.com/photos/wjserson/3310851114
  136. https://www.flickr.com/photos/joeshlabotnik/2384495536
  137. https://www.flickr.com/photos/much0/8552353901
  138. https://www.flickr.com/photos/thomashawk/10490113913
  139. References
  140. 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
  141. https://www.flickr.com/photos/drewm/3191872515
Advertisement