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

Building Testable PHP Applications

Building Testable PHP Applications

Slides from Midwest PHP 2013

Chris Hartjes

March 03, 2013
Tweet

More Decks by Chris Hartjes

Other Decks in Technology

Transcript

  1. LAW OF DEMETER “The Law of Demeter for functions states

    that any method of an object should call only methods belonging to itself, any parameters that were passed in to the method, any objects it created, and any directly held component objects. ”
  2. DEPENDENCY INJECTION <?php namespace Grumpy; class Acl { protected $_acls;

    ... public function accessAllowed() { $request = \Grumpy\Context::getRequest(); return ($acls[$request->getUri()] >= $_SESSION['user_level']); } } // Meanwhile inside your controller $acl = new \Grumpy\Acl(); if (!$acl->accessAllowed()) { \Grumpy\View::render('access_denied.tmpl'); } else { \Grumpy\View::render('show_stolen_cc.tmpl'); }
  3. DEPENDENCY INJECTION <?php namespace Grumpy; class Acl { ! protected

    $_acls; ! protected $_request; ! protected $_userLevel; ! ... ! public function __construct($request, $userLevel)! ! { ! ! ... ! ! ! ! $this->_request = $request; ! ! $this->_userLevel = $userLevel; ! } } // Meanwhile inside your controller $acl = new \Grumpy\Acl($this->request, $_SESSION['user_level']); if (!$acl->accessAllowed()) { ! \Grumpy\View::render('access_denied.tmpl'); } else { ! \Grumpy\View::render('show_stolen_cc.tmpl'); }
  4. DEPENDENCY INJECTION <?php namespace Grumpy; class Acl { ! ...

    ! public function setRequest($value) ! { ! ! $this->_request = $value; ! } ! public function setUserLevel($value) ! { ! ! $this->_userLevel = $value; ! } } // Meanwhile inside your controller... $acl = new \Grumpy\Acl(); $acl->setRequest($this->_request); $acl->setUserLevel($_SESSION['user_level']); if (!$acl->accessAllowed()) { ! \Grumpy\View::render('access_denied.tmpl'); } else { ! \Grumpy\View::render('show_stolen_cc.tmpl'); }
  5. HOW DO WE TEST THIS? <?php namespace Grumpy; class Acl

    { ! protected $_acls; ! protected $_request; ! protected $_userLevel; ! ... ! public function __construct($request, $userLevel)! ! { ! ! ... ! ! ! ! $this->_request = $request; ! ! $this->_userLevel = $userLevel; ! } } // Meanwhile inside your controller $acl = new \Grumpy\Acl($this->request, $_SESSION['user_level']); if (!$acl->accessAllowed()) { ! \Grumpy\View::render('access_denied.tmpl'); } else { ! \Grumpy\View::render('show_stolen_cc.tmpl'); }
  6. HOW DO WE TEST THIS? <?php class GrumpyAclTest extends \PHPUnit_Framework_TestCase

    { ! public function testAdminPurgeAccessAllowed() ! { ! ! $mockRequest = $this->getMockBuilder('\Grumpy\Controller\Request') ! ! ! ->disableOriginalConstructor() ! ! ! ->getMock(); ! ! $mockController->expects($this->once)) ! ! ! ->method('getUri') ! ! ! ->will($this->returnValue('/account/purge')); ! ! $testUserLevel = 'admin'; ! ! $acl = new \Grumpy\Acl($mockRequest, $testUserLevel); ! ! $this->assertTrue( ! ! ! $acl->accessAllowed(), ! ! ! 'admin user should have access to purge accounts' ! ! ); ! } }
  7. HOW DO WE TEST THIS? “Protected and private methods and

    attributes are difficult to test properly”
  8. METHODS? class ObjectWithPrivate { ! private function myInaccessiblePrivateMethod() ! {

    ! ! return 'inaccessible'; ! } ! /** @accessibleForTesting */ ! private function myAccessiblePrivateMethod() { ! ! return 'accessible'; ! } }
  9. METHODS? class ObjectWithPrivateTest extends PHPUnit_Framework_Testcase { ! public $accessible; !

    public function setUp() ! { ! ! parent::setUp(); ! ! $this->accessible = new PHPUnit_Extensions_Helper_AccessibleObject( ! ! ! new ObjectWithPrivate()); ! } ! public function testMyAccessiblePrivateMethod() ! { ! ! $this->assertEquals( ! ! ! 'accessible', ! ! ! $this->accessible->myAccessiblePrivateMethod() ! ! ); ! } }
  10. METHODS? class Foo { ! protected $_message; ! protected function

    _bar() ! { ! ! $this->_message = 'WRITE TESTS OR I CUT YOU'; ! } }
  11. METHODS? class FooTest extends PHPUnit_Framework_Testcase() { ! public function testProtectedBar()

    ! { ! ! $testFoo = new Foo(); ! ! $expectedMessage = 'WRITE TESTS OR I CUT YOU'; ! ! $reflectedFoo = new \ReflectionMethod($testFoo, '_bar'); ! ! $reflectedFoo->setAccessible(true); ! ! $reflectedFoo->invoke($testFoo); ! ! $testMessage = \PHPUnit_Framework_Assert::readAttribute( ! ! ! $testFoo, ! ! ! '_message') ! ! $this->assertEquals( ! ! ! $expectedMessage, ! ! ! $testMessage, ! ! ! "Did not get expected message" ! ! ); ! } }
  12. HOW DO YOU TEST THIS? “If your unit test actually

    uses the database, you are doing it wrong”
  13. HOW DO YOU TEST THIS? class Bar { ! public

    function getBazById($id) ! { ! ! $this->db->query("SELECT * FROM baz WHERE id = :bazId"); ! ! $this->db->bind('bazId', $id); ! ! $results = $this->db->execute(); ! ! $bazList = array(); ! ! if (count($results) > 0) { ! ! ! foreach ($results as $result) { ! ! ! ! $bazList[] = $result; ! ! ! } ! ! } ! ! return $bazList; ! } }
  14. HOW DO YOU TEST THIS? class BarTest extends PHPUnit_Framework_Testcase {

    ! public function testGetBazById() ! { ! ! $bazId = 666; ! ! $expectedResults= array(1, 2, 3, 4, 5); ! ! $mockDb = $this->getMockBuilder('\Grumpy\Db') ! ! ! ->disableOriginalConstructor() ! ! ! ->setMethods(array('query', 'execute', 'bind')) ! ! ! ->getMock(); ! ! $mockDb->expects($this->once()) ! ! ! ->method('query'); ! ! $mockDb->expects($this->once()) ! ! ! ->method('bind'); ! ! $mockDb->expects($this->once()) ! ! ! ->method('execute') ! ! ! ->will($this->returnValue($testResults)); ! ! $testBar = new Bar(); ! ! $testBar->setDb($mockDb); ! ! $testResults = $testBar->getBazById($bazId); ! ! $this->assertEquals( ! ! ! $expectedResults, ! ! ! $testResults, ! ! ! 'Did not get expected baz result set' ! ! ); ! } }
  15. HOW DO YOU TEST THIS? <?php class HipsterApi { !

    public function getBands() ! { ! ! return $this->_call('/api/bands', $this->_apiKey); ! } } class HipsterApiWrapper { ! public function __construct($hipsterApi) ! { ! ! $this->_hipsterApi = $hipsterApi; ! } ! public function getBands() ! { ! ! return $this->_hipsterApi->getBands(); ! } }
  16. HOW DO WE TEST THIS? class HipsterApiTest extends PHPUnit_Framework_Testcase {

    ! public function testGetBands() ! { ! ! $hipsterApiData = "[{'id': 17, 'Anonymous'}, {'id': 93, 'HipStaar'}]"; ! ! $mockHipsterApi = $this->getMockBuilder('HipsterApi') ! ! ! ->disableOriginalConstructor() ! ! ! ->getMock(); ! ! $mockHipsterApi->expects($this->once()) ! ! ! ->with('getBands') ! ! ! ->will($this->returnValue($hipsterApiData)); ! ! $expectedData = json_decode($hipsterApiData); ! ! $hipsterApiWrapper = new HipsterApiWrapper($mockHipsterApi); ! ! $testData = $hipsterApiWrapper->getBands(); ! ! ! ! ! $this->assertEquals( ! ! ! $expectedData, ! ! ! $testData, ! ! ! 'Did not get expected getBands() result from HipsterApi' ! ! ); ! } }
  17. WHAT NOT TO DO <?php class Presentation { public function

    speak() { $rant = Grumpy::getOpinion(); } }
  18. WHAT NOT TO DO 1. Get a Dependency Injection Container

    2. Create a fake version of your object with the static call 3. use the container inside the test
  19. WHAT NOT TO DO <?php class Presentation { public function

    convert($name) { $slides = new SlideDriver(); $slides->toPdf($name); } }
  20. WHAT NOT TO DO 1. Learn dependency injection 2. Create

    version of your object 3. Inject the dependency
  21. WHAT NOT TO DO <?php class Presentation { public function

    munge($frozbit) { $widgets = $frozbit ->what() ->is() ->this() ->i() ->dont() ->even(); } }
  22. WHAT NOT TO DO 1. Reduce the number of chains

    2. Reduce the amount of work