Building Testable PHP Applications

Building Testable PHP Applications

Slides from Midwest PHP 2013

27601bca8f38e75cbcf9d2dc843f0b32?s=128

Chris Hartjes

March 03, 2013
Tweet

Transcript

  1. Building Testable PHP Applications Chris Hartjes MidwestPHP 2013 - Mar.

    3, 2013 @grmpyprogrammer
  2. Text Story Time

  3. WHY DO WE TEST? Because programming is hard

  4. WHY DO WE TEST?

  5. A HUGE TOPIC

  6. UNCOMFORTABLE TRUTHS Some of this will not make sense to

    you
  7. UNCOMFORTABLE TRUTHS Some applications will resist all attempts to test

  8. UNCOMFORTABLE TRUTHS Testing is good Testable applications are better

  9. SO WHAT CAN WE DO?

  10. IT’S ABOUT TOOLS

  11. IT’S ABOUT STRATEGIES

  12. AUTOMATION IS KEY

  13. AUTOMATION IS KEY “Write a script that will run all

    your tests before you go live”
  14. AUTOMATION “Tell your version control system to run your tests

    on commit or push”
  15. AUTOMATION IS KEY http://jenkins-ci.org http://travis-ci.org

  16. ARCHITECTURE “Simple systems can display complex behavior but complex systems

    can only display simple behaviour”
  17. ARCHITECTURE “Inside every great large application are many great small

    applications”
  18. ARCHITECTURE “Your framework is a detail, not the core of

    your application.” -- Bob Martin
  19. 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. ”
  20. DEPENDENCY INJECTION “Pass objects and their methods other objects and

    function that are required for the task.”
  21. 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'); }
  22. 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'); }
  23. 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'); }
  24. MOCK OBJECTS “Mock objects allow you to test code in

    proper isolation”
  25. MOCK OBJECTS Database connections Web services File system operations

  26. 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'); }
  27. 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' ! ! ); ! } }
  28. HOW DO WE TEST THIS? “Protected and private methods and

    attributes are difficult to test properly”
  29. METHODS? Etsy’s PHPUnit Extensions https:/ /github.com/etsy/phpunit-extensions Uses annotations to flag

    methods you wish to test
  30. METHODS? class ObjectWithPrivate { ! private function myInaccessiblePrivateMethod() ! {

    ! ! return 'inaccessible'; ! } ! /** @accessibleForTesting */ ! private function myAccessiblePrivateMethod() { ! ! return 'accessible'; ! } }
  31. 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() ! ! ); ! } }
  32. METHODS? PHP’S Reflection API http:/ /www.gpug.ca/2012/06/02/testing- protected-methods-with-phpunit/

  33. METHODS? class Foo { ! protected $_message; ! protected function

    _bar() ! { ! ! $this->_message = 'WRITE TESTS OR I CUT YOU'; ! } }
  34. 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" ! ! ); ! } }
  35. ATTRIBUTES? PHP’S Reflection API PHPUnit lets you check attribute values

    but not set them
  36. HOW DO YOU TEST THIS? “If your unit test actually

    uses the database, you are doing it wrong”
  37. 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; ! } }
  38. 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' ! ! ); ! } }
  39. HOW DO YOU TEST THIS? “API calls should be done

    via wrapper methods”
  40. 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(); ! } }
  41. 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' ! ! ); ! } }
  42. ENVIRONMENTS “Your app shouldn’t care what environment it runs in.”

  43. ENVIRONMENTS “Keep config files for each environment under version control”

    https:/ /github.com/flogic/ whiskey_disk
  44. WHAT NOT TO DO

  45. WHAT NOT TO DO “Avoid the use of static method

    calls”
  46. WHAT NOT TO DO <?php class Presentation { public function

    speak() { $rant = Grumpy::getOpinion(); } }
  47. 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
  48. WHAT NOT TO DO “Avoid creating new non-core objects unless

    it’s a factory”
  49. WHAT NOT TO DO <?php class Presentation { public function

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

    version of your object 3. Inject the dependency
  51. WHAT NOT TO DO “Be careful with chaining methods”

  52. WHAT NOT TO DO <?php class Presentation { public function

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

    2. Reduce the amount of work
  54. RESOURCES The Grumpy Programmer’s Guide to Building Testable PHP Applications

    http:/ /grumpy-testing.com
  55. RESOURCES The Grumpy Programmer’s PHPUnit Cookbook http:/ /grumpy-phpunit.com

  56. RESOURCES The people sitting next to you

  57. THANK YOU! @grmpyprogrammer http:/ /www.littlehart.net/atthekeyboard https:/ /joind.in/8233