Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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 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].
  2. Reasons  why  not  to  test • No  4me   •

    No  budget   • We  deliver  tests  a;er  delivery  (  this  means  never  )   • We  don’t  know  how… 5
  3. Responsibility  issue • As  a  developer,  it’s  your  job  to

      • write  code  &  fixing  bugs   • add  documenta4on   • write  &  update  unit  tests 7
  4. 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
  5. 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
  6. 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
  7. Approach  for  tes-ng • Instan4ate  a  “unit-­‐of-­‐code”   • Assert

     expected  result  against  actual  result   • Provide  a  custom  error  message 13
  8. 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
  9. 17

  10. 18

  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. Dependency • __construct   • get_module_name   • get_version_min  

    • get_version_max   • is_sa4sfied_by   • requires   • requires_exact   • requires_at_least   • requires_range 53
  22. 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
  23. 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
  24. 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
  25. 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
  26. 70 joind.in/11363! ! If you liked my talk, thanks. If

    not, let me know how to improve it