Pro Yearly is on sale from $80 to $50! »

Your code are my tests

Your code are my tests

After years of promoting PHPUnit I still hear it's hard to get started with unit testing. So instead of showing nice step-by-step examples on how to use PHPUnit, we're going to take an example straight from github.

So I take on the challenge to start writing tests for PHP projects that don't have unit tests in place and explain how I decide where to begin, how I approach my test strategy and how I ensure I’m covering each possible use case (and covering the CRAP index).

The goal of this presentation is to show everyone that even legacy code, spaghetti code and complex code bases can be tested. After this talk you can immediately apply my examples on your own codebase (even if it's a clean code base) and get started with testing.

8fc45f4725efe8e8bc8d6c1f92224b65?s=128

Michelangelo

June 03, 2014
Tweet

Transcript

  1. 2 Your  code  are  my  tests PHP-­‐WVL   Brugge,  Belgium

  2. 2 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@PHP-WVL.BE.
  3. 3 Michelangelo van Dam! ! PHP Consultant Community Leader President

    of PHPBenelux Contributor
  4. Why  bother  with  tes-ng? 4 https://www.flickr.com/photos/vialbost/5533266530

  5. Reasons  why  not  to  test • No  4me   •

    No  budget   • We  deliver  tests  a;er  delivery  (  this  means  never  )   • We  don’t  know  how… 5
  6. No  excuses!!! 6 Crea4ve  Commons  -­‐  hFp://www.flickr.com/photos/akrabat/8421560178

  7. Responsibility  issue • As  a  developer,  it’s  your  job  to

      • write  code  &  fixing  bugs   • add  documenta4on   • write  &  update  unit  tests 7
  8. Pizza  principle 8 Topping:  your  tests Box:  your  documenta4on Dough:

     your  code
  9. Benefits  of  tes-ng • Direct  feedback  (test  fails)   •

    Once  a  test  is  made,  it  will  always  be  tested   • Easy  to  refactor  exis4ng  code  (protec4on)   • Easy  to  debug:  write  a  test  to  see  if  a  bug  is  genuine   • Higher  confidence  and  less  uncertainty 9
  10. 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
  11. Warming  up 11 https://www.flickr.com/photos/bobjagendorf/8535316836

  12. PHPUnit • PHPUnit  is  a  port  of  xUnit  tes4ng  framework

      • Created  by  “Sebas4an  Bergmann”   • Uses  “asser4ons”  to  verify  behaviour  of  “unit  of  code”   • Open  source  and  hosted  on  GitHub   • See  hFps://github.com/sebas4anbergmann/phpunit   • Can  be  installed  using:   • PEAR   • PHAR   • Composer 12
  13. Approach  for  tes-ng • Instan4ate  a  “unit-­‐of-­‐code”   • Assert

     expected  result  against  actual  result   • Provide  a  custom  error  message 13
  14. Available  asser-ons • assertArrayHasKey()   • assertClassHasAFribute()   • assertClassHasSta4cAFribute()

      • assertContains()   • assertContainsOnly()   • assertContainsOnlyInstancesOf()   • assertCount()   • assertEmpty()   • assertEqualXMLStructure()   • assertEquals()   • assertFalse()   • assertFileEquals()   • assertFileExists()   • assertGreaterThan()   • assertGreaterThanOrEqual()   • assertInstanceOf()   • assertInternalType()   • assertJsonFileEqualsJsonFile()   • assertJsonStringEqualsJsonFile()   • assertJsonStringEqualsJsonString()   • assertLessThan()   • assertLessThanOrEqual()   • assertNull()   • assertObjectHasAFribute()   • assertRegExp()   • assertStringMatchesFormat()   • assertStringMatchesFormatFile()   • assertSame()   • assertSelectCount()   • assertSelectEquals()   • assertSelectRegExp()   • assertStringEndsWith()   • assertStringEqualsFile()   • assertStringStartsWith()   • assertTag()   • assertThat()   • assertTrue()   • assertXmlFileEqualsXmlFile()   • assertXmlStringEqualsXmlFile()   • assertXmlStringEqualsXmlString() 14
  15. To  protect  and  to  serve 15

  16. Data  is  tainted,  ALWAYS 16 Hack er s BAD DATA

    W eb Services Stupid users
  17. 17

  18. 18

  19. OWASP  top  10  exploits 19 https://www.owasp.org/index.php/Top_10_2013-Top_10

  20. Filtering  &  Valida-on 20

  21. Smallest  unit  of  code 21 https://www.flickr.com/photos/toolstop/4546017269

  22. 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
  23. Tes-ng  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
  24. Tes-ng  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
  25. We  don’t  live  in  a  fairy  tale! 25 https://www.flickr.com/photos/bertknot/8175214909

  26. Real  code,  real  apps 26

  27. github.com/Telaxus/EPESI 27

  28. Running  the  project 28

  29. Where  are  the  TESTS? 29

  30. Where  are  the  TESTS? 30

  31. Oh  noes,  no  tests! 31 https://www.flickr.com/photos/mjhagen/2973212926

  32. Let’s  get  started 32 https://www.flickr.com/photos/npobre/2601582256

  33. How  to  get  about  it? 33

  34. Se\ng  up  for  tes-ng <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> 34
  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   • sa4sfy_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 35
  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; ! } 36
  37. Tes-ng  first  condi-on <?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'); ! } ! } 37
  38. Run  test 38

  39. Check  coverage 39

  40. Test  for  second  condi-on 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'); ! } 40
  41. Run  tests 41

  42. Check  coverage 42

  43. Test  for  third  condi-on 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'); ! } 43
  44. Run  tests 44

  45. Check  code  coverage 45

  46. Non-­‐executable  code 46 https://www.flickr.com/photos/dazjohnson/7720806824

  47. 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'); ! } 47
  48. Run  tests 48

  49. Check  code  coverage 49

  50. Look  at  the  global  coverage 50

  51. Bridging  gaps 51 https://www.flickr.com/photos/hugo90/6980712643

  52. Privates  exposed 52 http://www.slashgear.com/former-tsa-agent-admits-we-knew-full-body-scanners-didnt-work-31315288/

  53. Dependency • __construct   • get_module_name   • get_version_min  

    • get_version_max   • is_sa4sfied_by   • requires   • requires_exact   • requires_at_least   • requires_range 53
  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 ? '<=' : '<'; ! } ! ! /** ... */ ! } 54
  55. Don’t  touch  my  junk! 55 https://www.flickr.com/photos/caseymultimedia/5412293730

  56. House  of  Reflec-on 56 https://www.flickr.com/photos/tabor-roeder/8250770115

  57. 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!! } ! } 57
  58. Let’s  use  the  sta-c $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'); 58
  59. Use  the  reflec-on  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'); 59
  60. Run  tests 60

  61. Code  Coverage 61

  62. Yes,  paradise  exists 62 https://www.flickr.com/photos/rnugraha/2003147365

  63. Unit  tes-ng  is  not  difficult! 63

  64. You  just  need  to  get  started 64

  65. PHP  has  all  the  tools 65

  66. And  there  are  more     roads  to  Rome 66

  67. Recommended  reading 67

  68. Need  help? 68 Michelangelo van Dam ! michelangelo@in2it.be @DragonBe www.in2it.be

  69. Ques-ons? 69 https://www.flickr.com/photos/mdpettitt/8671901426

  70. 70 joind.in/11363! ! If you liked my talk, thanks. If

    not, let me know how to improve it