Testing untestable Code
   Stephan Hochdörfer, bitExpert AG




 "Quality is a function of thought and reflection -
 precise thought and reflection. That’s the magic."
                 Michael Feathers
Agenda
1.   About me

2.   Theory

3.   How to test untestable code

4.   Generating testable code

5.   Conclusions

6.   Questions
About me
 Stephan Hochdörfer, bitExpert AG

 Department Manager Research Labs

 enjoying PHP since 1999

 S.Hochdoerfer@bitExpert.de

 @shochdoerfer
No excuse for bad code!
Warning! Use at own risk...
Theory




         "There is no secret to writing tests, there
          are only secrets to write testable code!"
                         Miško Hevery
Theory



 What is „untestable code“?
Theory



 What is „untestable code“?
Theory



 What is „untestable code“?
Theory




          "...our test strategy requires us to have more control or
         visibility of the internal behavior of the system under test."
               Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
Theory




                               Required
                                class
                    Class to
         Unittest
                      Test
                               Required
                                class
Theory




                                          Database

                               Required
                                class
                                           External
                    Class to
         Unittest                          resource
                      test
                               Required
                                class




                    Required   Required
                                           Webservice
                     class      class
Theory




                                          Database

                               Required
                                class
                                           External
                    Class to
         Unittest                          resource
                      test
                               Required
                                class




                    Required   Required
                                           Webservice
                     class      class
Theory



 How to achieve „testable“ code?
Theory



 How to achieve „testable“ code?




                Refactoring
Theory




         "Before you start refactoring, check that you
                  have a solid suite of tests."
                     Martin Fowler, Refactoring
Testing „untestable“ PHP Code



 Let the work begin...
Testing „untestable“ PHP Code



 For your safty!




          Do not change existing code!
Testing „untestable“ PHP Code | __autoload




<?php
class Car {
    private $Engine;

     public function __construct($sEngine) {
         $this->Engine = Engine::getByType($sEngine);
     }

}
Testing „untestable“ PHP Code | __autoload




<?php
class Car {
    private $Engine;

     public function __construct($sEngine) {
         $this->Engine = Engine::getByType($sEngine);
     }

}




How to inject a dependency?
 Use __autoload
Testing „untestable“ PHP Code | __autoload




<?php
function run_autoload($psClass) {
    $sFileToInclude = strtolower($psClass).'.php';
    if(strtolower($psClass) == 'engine') {
         $sFileToInclude = '/custom/mocks/'.$sFileToInclude;
    }
    include($sFileToInclude);
}


// Testcase
spl_autoload_register('run_autoload');
$oCar = new Car('Diesel');
echo $oCar->run();
Testing „untestable“ PHP Code | include_path




<?php
include('Engine.php');

class Car {
    private $Engine;

     public function __construct($sEngine) {
         $this->Engine = Engine::getByType($sEngine);
     }
}
Testing „untestable“ PHP Code | include_path




<?php
include('Engine.php');

class Car {
    private $Engine;

     public function __construct($sEngine) {
         $this->Engine = Engine::getByType($sEngine);
     }
}




How to inject a dependency?
 Manipulate include_path setting
Testing „untestable“ PHP Code | include_path




<?php
ini_set('include_path',
    '/custom/mocks/'.PATH_SEPARATOR.
    ini_get('include_path'));

// Testcase
include('car.php');

$oCar = new Car('Diesel');
echo $oCar->run();
Testing „untestable“ PHP Code | include_path alternative

<?php
class CustomFileStreamWrapper {
  private $_handler;

    function stream_open($path, $mode, $options, &$opened_path) {
      stream_wrapper_restore('file');
       // @TODO: modify $path before fopen
      $this->_handler = fopen($path, $mode);
      stream_wrapper_unregister('file');
      stream_wrapper_register('file', 'CustomFileStreamWrapper');
      return true;
    }

    function stream_read($count) {}

    function stream_write($data) {}

    function stream_tell() {}

    function stream_eof() {}

    function stream_seek($offset, $whence) {}
}

stream_wrapper_unregister('file');
stream_wrapper_register('file', 'CustomFileStreamWrapper');



Source: Alex Netkachov, http://www.alexatnet.com/node/203
Testing „untestable“ PHP Code | include_path alternative

<?php
class CustomFileStreamWrapper {
      private $_handler;

       function stream_open($path, $mode, $options, &$opened_path) {
            stream_wrapper_restore('file');
            $this->_handler = fopen($path, $mode);
            stream_wrapper_unregister('file');
            stream_wrapper_register('file', 'CustomFileStreamWrapper');
            return true;
       }

       function stream_read($count) {
            $content = fread($this->_handler, $count);
            $content = str_replace('Engine::getByType', 'MockedEngine::get', $content);
            return $content;
       }
}

stream_wrapper_unregister('file');
stream_wrapper_register('file', 'CustomFileStreamWrapper');

include('engine.php');
?>
Testing „untestable“ PHP Code | Namespaces




<?php
class Car {
    private $Engine;

     public function __construct($sEngine) {
         $this->Engine = CarEngine::getByType($sEngine);
     }
}
Testing „untestable“ PHP Code | Namespaces




<?php
class Car {
    private $Engine;

     public function __construct($sEngine) {
         $this->Engine = CarEngine::getByType($sEngine);
     }
}




How to inject a dependency?
 Use __autoload or manipulate the include_path
Testing „untestable“ PHP Code | vfsStream




<?php
class Car {
    private $Engine;

     public function __construct($sEngine, $CacheDir) {
         $this->Engine = CarEngine::getByType($sEngine);
         mkdir($CacheDir.'/cache/', 0700, true);
     }
}
Testing „untestable“ PHP Code | vfsStream




<?php
class Car {
    private $Engine;

     public function __construct($sEngine, $CacheDir) {
         $this->Engine = CarEngine::getByType($sEngine);
         mkdir($CacheDir.'/cache/', 0700, true);
     }
}




How mock a filesystem?
 Use vfsStream - http://code.google.com/p/bovigo/
Testing „untestable“ PHP Code | vfsStream




<?php

// setup vfsStream
vfsStreamWrapper::register();
vfsStreamWrapper::setRoot(new vfsStreamDirectory('app'));

$oCar = new Car('Diesel', vfsStream::url('app'));

echo vfsStreamWrapper::getRoot()->hasChild('cache');
Testing „untestable“ PHP Code




       „I have no idea how to unit-test procedural code. Unit-testing
          assumes that I can instantiate a piece of my application in
                                  isolation.“
                                 Miško Hevery
Testing „untestable“ PHP Code | Test functions




<?php
function startsWith($sString, $psPre) {
    return $psPre == substr($sString, 0, strlen($psPre));
}

function contains($sString, $sSearch) {
    return false !== strpos($sString, $sSearch);
}
Testing „untestable“ PHP Code | Test functions




<?php
function startsWith($sString, $psPre) {
    return $psPre == substr($sString, 0, strlen($psPre));
}

function contains($sString, $sSearch) {
    return false !== strpos($sString, $sSearch);
}




How to test
 PHPUnit can call functions
 PHPUnit can save/restore globale state
Testing „untestable“ PHP Code | overwrite internal functions




<?php
function buyCar(Car $oCar) {
    global $oDB;

     mysql_query("INSERT INTO...", $oDB);

     mail('order@domain.org', 'New sale', '....');
}
Testing „untestable“ PHP Code | overwrite internal functions




<?php
function buyCar(Car $oCar) {
    global $oDB;

     mysql_query("INSERT INTO...", $oDB);

     mail('order@domain.org', 'New sale', '....');
}




How to test
 Do not load mysql extension. Provide own implementation
 Unfortunatley mail() is part of the PHP core
Testing „untestable“ PHP Code | overwrite internal functions




<?php
function buyCar(Car $oCar) {
    global $oDB;

     mysql_query("INSERT INTO...", $oDB);

     mail('order@domain.org', 'New sale', '....');
}




How to test
 Use classkit extension to overwrite internal functions
Testing „untestable“ PHP Code | overwrite internal functions




<?php

ini_set('runkit.internal_override', '1');

runkit_function_redefine('mail','','return true;');

?>
Testing „untestable“ PHP Code
Generating testable code



 Generative Programming
Generating testable code



 Generative Programming

                             Configuration




                                                         1 ... n
            Implementation
                                             Generator    Product
             components




                               Generator
                               application
Generating testable code



 Generative Programming

                             Configuration


                                                         Application



            Implementation
                                             Generator
             components



                                                         Testcases
                               Generator
                               application
Generating testable code



 Course of action
 Extraction
  „Mask“ parts of the code


 Customizing
  Change content of global vars
  Pre/Postfixes for own functions, methods, classes


 Recombine
  Re-order parts of the code
Generating testable code




FileFrm FILEIndex_php5 {
  private String Prefix   = "test_";
  private String MailSlot = "mail('order@domain.org', 'New sale', '....');";

   public FILEIndex_php5() {
     setFilename("index.php5");
     setRelativePath("/");
   }

  private void assign() {
BEGINCONTENT()
<?php
function buyCar(Car $oCar) {
global $oDB;

<!{Prefix}!>mysql_query(„INSERT INTO...“, $oDB);
<!{MailSlot}!>
}

?>
ENDCONTENT()
  }
}
Generating testable code




1. Example
Prefix: test_
<?php
function buyCar(Car $oCar) {
  global $oDB;

    test_mysql_query("INSERT INTO...", $oDB);
}
Generating testable code




1. Example
Prefix: test_
<?php
function buyCar(Car $oCar) {
  global $oDB;

     test_mysql_query("INSERT INTO...", $oDB);
}


2. Example
MailSlot: mail('order@domain.org', 'New sale', '....');
<?php
function buyCar(Car $oCar) {
  global $oDB;

     mysql_query("INSERT INTO...", $oDB);
     mail('order@domain.org', 'New sale', '....');
}

?>
Conclusion



 How much effort to take?
Conclusion



 Conclusion

  Change mindset to write testable code
        
          Dependency Injection

  Look for other options to raise the bar
         
           Work around limitations of PHP
         
           PHP is flexible, use it that way
http://joind.in/1545

Testing untestable code - DPC10

  • 1.
    Testing untestable Code Stephan Hochdörfer, bitExpert AG "Quality is a function of thought and reflection - precise thought and reflection. That’s the magic." Michael Feathers
  • 2.
    Agenda 1. About me 2. Theory 3. How to test untestable code 4. Generating testable code 5. Conclusions 6. Questions
  • 3.
    About me  StephanHochdörfer, bitExpert AG  Department Manager Research Labs  enjoying PHP since 1999  S.Hochdoerfer@bitExpert.de  @shochdoerfer
  • 4.
    No excuse forbad code!
  • 5.
    Warning! Use atown risk...
  • 6.
    Theory "There is no secret to writing tests, there are only secrets to write testable code!" Miško Hevery
  • 7.
    Theory What is„untestable code“?
  • 8.
    Theory What is„untestable code“?
  • 9.
    Theory What is„untestable code“?
  • 10.
    Theory "...our test strategy requires us to have more control or visibility of the internal behavior of the system under test." Gerard Meszaros, xUnit Test Patterns: Refactoring Test Code
  • 11.
    Theory Required class Class to Unittest Test Required class
  • 12.
    Theory Database Required class External Class to Unittest resource test Required class Required Required Webservice class class
  • 13.
    Theory Database Required class External Class to Unittest resource test Required class Required Required Webservice class class
  • 14.
    Theory How toachieve „testable“ code?
  • 15.
    Theory How toachieve „testable“ code? Refactoring
  • 16.
    Theory "Before you start refactoring, check that you have a solid suite of tests." Martin Fowler, Refactoring
  • 17.
    Testing „untestable“ PHPCode Let the work begin...
  • 18.
    Testing „untestable“ PHPCode For your safty! Do not change existing code!
  • 19.
    Testing „untestable“ PHPCode | __autoload <?php class Car { private $Engine; public function __construct($sEngine) { $this->Engine = Engine::getByType($sEngine); } }
  • 20.
    Testing „untestable“ PHPCode | __autoload <?php class Car { private $Engine; public function __construct($sEngine) { $this->Engine = Engine::getByType($sEngine); } } How to inject a dependency?  Use __autoload
  • 21.
    Testing „untestable“ PHPCode | __autoload <?php function run_autoload($psClass) { $sFileToInclude = strtolower($psClass).'.php'; if(strtolower($psClass) == 'engine') { $sFileToInclude = '/custom/mocks/'.$sFileToInclude; } include($sFileToInclude); } // Testcase spl_autoload_register('run_autoload'); $oCar = new Car('Diesel'); echo $oCar->run();
  • 22.
    Testing „untestable“ PHPCode | include_path <?php include('Engine.php'); class Car { private $Engine; public function __construct($sEngine) { $this->Engine = Engine::getByType($sEngine); } }
  • 23.
    Testing „untestable“ PHPCode | include_path <?php include('Engine.php'); class Car { private $Engine; public function __construct($sEngine) { $this->Engine = Engine::getByType($sEngine); } } How to inject a dependency?  Manipulate include_path setting
  • 24.
    Testing „untestable“ PHPCode | include_path <?php ini_set('include_path', '/custom/mocks/'.PATH_SEPARATOR. ini_get('include_path')); // Testcase include('car.php'); $oCar = new Car('Diesel'); echo $oCar->run();
  • 25.
    Testing „untestable“ PHPCode | include_path alternative <?php class CustomFileStreamWrapper { private $_handler; function stream_open($path, $mode, $options, &$opened_path) { stream_wrapper_restore('file'); // @TODO: modify $path before fopen $this->_handler = fopen($path, $mode); stream_wrapper_unregister('file'); stream_wrapper_register('file', 'CustomFileStreamWrapper'); return true; } function stream_read($count) {} function stream_write($data) {} function stream_tell() {} function stream_eof() {} function stream_seek($offset, $whence) {} } stream_wrapper_unregister('file'); stream_wrapper_register('file', 'CustomFileStreamWrapper'); Source: Alex Netkachov, http://www.alexatnet.com/node/203
  • 26.
    Testing „untestable“ PHPCode | include_path alternative <?php class CustomFileStreamWrapper { private $_handler; function stream_open($path, $mode, $options, &$opened_path) { stream_wrapper_restore('file'); $this->_handler = fopen($path, $mode); stream_wrapper_unregister('file'); stream_wrapper_register('file', 'CustomFileStreamWrapper'); return true; } function stream_read($count) { $content = fread($this->_handler, $count); $content = str_replace('Engine::getByType', 'MockedEngine::get', $content); return $content; } } stream_wrapper_unregister('file'); stream_wrapper_register('file', 'CustomFileStreamWrapper'); include('engine.php'); ?>
  • 27.
    Testing „untestable“ PHPCode | Namespaces <?php class Car { private $Engine; public function __construct($sEngine) { $this->Engine = CarEngine::getByType($sEngine); } }
  • 28.
    Testing „untestable“ PHPCode | Namespaces <?php class Car { private $Engine; public function __construct($sEngine) { $this->Engine = CarEngine::getByType($sEngine); } } How to inject a dependency?  Use __autoload or manipulate the include_path
  • 29.
    Testing „untestable“ PHPCode | vfsStream <?php class Car { private $Engine; public function __construct($sEngine, $CacheDir) { $this->Engine = CarEngine::getByType($sEngine); mkdir($CacheDir.'/cache/', 0700, true); } }
  • 30.
    Testing „untestable“ PHPCode | vfsStream <?php class Car { private $Engine; public function __construct($sEngine, $CacheDir) { $this->Engine = CarEngine::getByType($sEngine); mkdir($CacheDir.'/cache/', 0700, true); } } How mock a filesystem?  Use vfsStream - http://code.google.com/p/bovigo/
  • 31.
    Testing „untestable“ PHPCode | vfsStream <?php // setup vfsStream vfsStreamWrapper::register(); vfsStreamWrapper::setRoot(new vfsStreamDirectory('app')); $oCar = new Car('Diesel', vfsStream::url('app')); echo vfsStreamWrapper::getRoot()->hasChild('cache');
  • 32.
    Testing „untestable“ PHPCode „I have no idea how to unit-test procedural code. Unit-testing assumes that I can instantiate a piece of my application in isolation.“ Miško Hevery
  • 33.
    Testing „untestable“ PHPCode | Test functions <?php function startsWith($sString, $psPre) { return $psPre == substr($sString, 0, strlen($psPre)); } function contains($sString, $sSearch) { return false !== strpos($sString, $sSearch); }
  • 34.
    Testing „untestable“ PHPCode | Test functions <?php function startsWith($sString, $psPre) { return $psPre == substr($sString, 0, strlen($psPre)); } function contains($sString, $sSearch) { return false !== strpos($sString, $sSearch); } How to test  PHPUnit can call functions  PHPUnit can save/restore globale state
  • 35.
    Testing „untestable“ PHPCode | overwrite internal functions <?php function buyCar(Car $oCar) { global $oDB; mysql_query("INSERT INTO...", $oDB); mail('order@domain.org', 'New sale', '....'); }
  • 36.
    Testing „untestable“ PHPCode | overwrite internal functions <?php function buyCar(Car $oCar) { global $oDB; mysql_query("INSERT INTO...", $oDB); mail('order@domain.org', 'New sale', '....'); } How to test  Do not load mysql extension. Provide own implementation  Unfortunatley mail() is part of the PHP core
  • 37.
    Testing „untestable“ PHPCode | overwrite internal functions <?php function buyCar(Car $oCar) { global $oDB; mysql_query("INSERT INTO...", $oDB); mail('order@domain.org', 'New sale', '....'); } How to test  Use classkit extension to overwrite internal functions
  • 38.
    Testing „untestable“ PHPCode | overwrite internal functions <?php ini_set('runkit.internal_override', '1'); runkit_function_redefine('mail','','return true;'); ?>
  • 39.
  • 40.
    Generating testable code Generative Programming
  • 41.
    Generating testable code Generative Programming Configuration 1 ... n Implementation Generator Product components Generator application
  • 42.
    Generating testable code Generative Programming Configuration Application Implementation Generator components Testcases Generator application
  • 43.
    Generating testable code Course of action Extraction  „Mask“ parts of the code Customizing  Change content of global vars  Pre/Postfixes for own functions, methods, classes Recombine  Re-order parts of the code
  • 44.
    Generating testable code FileFrmFILEIndex_php5 { private String Prefix = "test_"; private String MailSlot = "mail('order@domain.org', 'New sale', '....');"; public FILEIndex_php5() { setFilename("index.php5"); setRelativePath("/"); } private void assign() { BEGINCONTENT() <?php function buyCar(Car $oCar) { global $oDB; <!{Prefix}!>mysql_query(„INSERT INTO...“, $oDB); <!{MailSlot}!> } ?> ENDCONTENT() } }
  • 45.
    Generating testable code 1.Example Prefix: test_ <?php function buyCar(Car $oCar) { global $oDB; test_mysql_query("INSERT INTO...", $oDB); }
  • 46.
    Generating testable code 1.Example Prefix: test_ <?php function buyCar(Car $oCar) { global $oDB; test_mysql_query("INSERT INTO...", $oDB); } 2. Example MailSlot: mail('order@domain.org', 'New sale', '....'); <?php function buyCar(Car $oCar) { global $oDB; mysql_query("INSERT INTO...", $oDB); mail('order@domain.org', 'New sale', '....'); } ?>
  • 47.
    Conclusion How mucheffort to take?
  • 48.
    Conclusion Conclusion Change mindset to write testable code  Dependency Injection  Look for other options to raise the bar  Work around limitations of PHP  PHP is flexible, use it that way
  • 49.