$30 off During Our Annual Pro Sale. View Details »

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.

DragonBe

June 03, 2014
Tweet

More Decks by DragonBe

Other Decks in Technology

Transcript

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

    View Slide

  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 [email protected].

    View Slide

  3. 3
    Michelangelo van Dam!
    !
    PHP Consultant
    Community Leader
    President of PHPBenelux
    Contributor

    View Slide

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

    View Slide

  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

    View Slide

  6. No  excuses!!!
    6
    Crea4ve  Commons  -­‐  hFp://www.flickr.com/photos/akrabat/8421560178

    View Slide

  7. Responsibility  issue
    • As  a  developer,  it’s  your  job  to  
    • write  code  &  fixing  bugs  
    • add  documenta4on  
    • write  &  update  unit  tests
    7

    View Slide

  8. Pizza  principle
    8
    Topping:  your  tests
    Box:  your  documenta4on
    Dough:  your  code

    View Slide

  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

    View Slide

  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

    View Slide

  11. Warming  up
    11
    https://www.flickr.com/photos/bobjagendorf/8535316836

    View Slide

  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

    View Slide

  13. Approach  for  tes-ng
    • Instan4ate  a  “unit-­‐of-­‐code”  
    • Assert  expected  result  against  actual  result  
    • Provide  a  custom  error  message
    13

    View Slide

  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

    View Slide

  15. To  protect  and  to  serve
    15

    View Slide

  16. Data  is  tainted,  ALWAYS
    16
    Hack
    er
    s BAD DATA
    W
    eb Services
    Stupid users

    View Slide

  17. 17

    View Slide

  18. 18

    View Slide

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

    View Slide

  20. Filtering  &  Valida-on
    20

    View Slide

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

    View Slide

  22. Example  class
    /** !
    * 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

    View Slide

  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

    View Slide

  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

    View Slide

  25. We  don’t  live  in  a  fairy  tale!
    25
    https://www.flickr.com/photos/bertknot/8175214909

    View Slide

  26. Real  code,  real  apps
    26

    View Slide

  27. github.com/Telaxus/EPESI
    27

    View Slide

  28. Running  the  project
    28

    View Slide

  29. Where  are  the  TESTS?
    29

    View Slide

  30. Where  are  the  TESTS?
    30

    View Slide

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

    View Slide

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

    View Slide

  33. How  to  get  about  it?
    33

    View Slide

  34. Se\ng  up  for  tes-ng
    !
    !
    !
    tests/admin!
    !
    !
    tests/include!
    !
    !
    tests/modules!
    !
    !
    !
    !
    !
    !
    !
    !
    !
    !
    !

    34

    View Slide

  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

    View Slide

  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

    View Slide

  37. Tes-ng  first  condi-on
    !
    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

    View Slide

  38. Run  test
    38

    View Slide

  39. Check  coverage
    39

    View Slide

  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

    View Slide

  41. Run  tests
    41

    View Slide

  42. Check  coverage
    42

    View Slide

  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

    View Slide

  44. Run  tests
    44

    View Slide

  45. Check  code  coverage
    45

    View Slide

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

    View Slide

  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

    View Slide

  48. Run  tests
    48

    View Slide

  49. Check  code  coverage
    49

    View Slide

  50. Look  at  the  global  coverage
    50

    View Slide

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

    View Slide

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

    View Slide

  53. Dependency
    • __construct  
    • get_module_name  
    • get_version_min  
    • get_version_max  
    • is_sa4sfied_by  
    • requires  
    • requires_exact  
    • requires_at_least  
    • requires_range
    53

    View Slide

  54. A  private  constructor!
    !
    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

    View Slide

  55. Don’t  touch  my  junk!
    55
    https://www.flickr.com/photos/caseymultimedia/5412293730

    View Slide

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

    View Slide

  57. Let’s  do  this…
    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

    View Slide

  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

    View Slide

  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

    View Slide

  60. Run  tests
    60

    View Slide

  61. Code  Coverage
    61

    View Slide

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

    View Slide

  63. Unit  tes-ng  is  not  difficult!
    63

    View Slide

  64. You  just  need  to  get  started
    64

    View Slide

  65. PHP  has  all  the  tools
    65

    View Slide

  66. And  there  are  more    
    roads  to  Rome
    66

    View Slide

  67. Recommended  reading
    67

    View Slide

  68. Need  help?
    68
    Michelangelo van Dam
    !
    [email protected]
    @DragonBe
    www.in2it.be

    View Slide

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

    View Slide

  70. 70
    joind.in/11363!
    !
    If you liked my talk, thanks.
    If not, let me know how to improve it

    View Slide