The document discusses testing untestable code. It begins by defining untestable code as code that is difficult to test in isolation without dependencies like databases or external resources. It then provides several techniques for making code more testable such as using dependency injection, manipulating include paths, and mocking dependencies. Specific examples are given for techniques like overriding internal functions and using stream wrappers to mock includes. The talk emphasizes writing testable code from the beginning through principles like dependency injection and avoiding direct dependencies where possible.
Große Systeme, lose Kopplung, Spaß bei der Arbeit! - WDC12
Testing untestable PHP code
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. About me
Stephan Hochdörfer, bitExpert AG
Department Manager Research Labs
enjoying PHP since 1999
S.Hochdoerfer@bitExpert.de
@shochdoerfer
11. 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
12. Theory
Required
Required
class
class
Class to
Unittest Class to
Unittest Test
Test
Required
Required
class
class
13. Theory
Database
Database
Required
Required
class
class
External
Class to External
Unittest Class to resource
Unittest test resource
test
Required
Required
class
class
Required Required
Required Required Webservice
class class Webservice
class class
14. Theory
Database
Database
Required
Required
class
class
External
Class to External
Unittest Class to resource
Unittest test resource
test
Required
Required
class
class
Required Required
Required Required Webservice
class class Webservice
class class
20. Testing „untestable“ PHP Code | __autoload
<?php
class Car {
private $Engine;
public function __construct($sEngine) {
$this->Engine = Engine::getByType($sEngine);
}
}
21. 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
23. Testing „untestable“ PHP Code | include_path
<?php
include('Engine.php');
class Car {
private $Engine;
public function __construct($sEngine) {
$this->Engine = Engine::getByType($sEngine);
}
}
24. 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
26. Testing „untestable“ PHP Code | include_path alternative
<?php
include('Engine.php');
class Car {
private $Engine;
public function __construct($sEngine) {
$this->Engine = Engine::getByType($sEngine);
}
}
27. Testing „untestable“ PHP Code | include_path alternative
<?php
include('Engine.php');
class Car {
private $Engine;
public function __construct($sEngine) {
$this->Engine = Engine::getByType($sEngine);
}
}
How to inject a dependency?
Custom Stream Wrapper behaviour
Idea by Alex Netkachov, http://www.alexatnet.com/node/203
28. 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');
34. Testing „untestable“ PHP Code | Namespaces
<?php
class Car {
private $Engine;
public function __construct($sEngine) {
$this->Engine = CarEngine::getByType($sEngine);
}
}
35. 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
36. 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);
}
}
37. 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/
39. Testing „untestable“ PHP Code | Database
Database Testing
Use the methods provided by your favourite framework
e.g Zend Framework
Implement Zend_Db_Statement_Interface
subclass Zend_Db_Adapter_Abstract
$db = new Custom_Db_Adapter(array());
Zend_Db_Table::setDefaultAdapter($db);
40. Testing „untestable“ PHP Code | Database
Database Testing
For low-level database access:
e.g MySQL
do not load the mysql extension
Add custom userland implementations of mysql_* functions
41. Testing „untestable“ PHP Code | Database
Database Testing
Use the methods provided by your favourite unittest framework
e.g PHPUnit
extend PHPUnit_Extensions_Database_TestCase
implement getConnection() and getDataset()
42. Testing „untestable“ PHP Code | Database
Database Testing
Use the methods provided by your favourite sql server (and tools)
e.g MySQL
use MySQL Proxy to transparently switch databases
Begin and rollback transactions
43. 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
44. 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);
}
45. 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
46. 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
49. 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
Unfortunatley mail() is part of the PHP core and cannot be unloaded
50. 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
60. Generating testable code
Generative Programming
Extraction
Show / hide parts of the code
Example
MailSlot: mail('order@domain.org', 'New sale', '....');
61. Generating testable code
Generative Programming
Extraction
Show / hide parts of the code
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', '....');
}
?>
62. Generating testable code
Course of action
Customizing
Change content of global vars
Pre/Postfixes for own functions, methods, classes
Example
Prefix: test_
63. Generating testable code
Course of action
Customizing
Change content of global vars
Pre/Postfixes for own functions, methods, classes
Example
Prefix: test_
<?php
function buyCar(Car $oCar) {
global $oDB;
test_mysql_query("INSERT INTO...", $oDB);
}