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

Understanding The Helter Skelter World of Building Testable PHP Applications

Understanding The Helter Skelter World of Building Testable PHP Applications

A talk about the basics of getting your PHP applications to a state so you can write unit tests for them.

Chris Hartjes

January 18, 2014
Tweet

More Decks by Chris Hartjes

Other Decks in Technology

Transcript

  1. January 18, 2014
    Understanding The Helter Skelter World
    of Building Testable PHP Applications
    by Chris Hartjes - SkiPHP 2014

    View Slide

  2. 01
    Who is the guy?

    View Slide

  3. 01

    View Slide

  4. 2002

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. View Slide

  9. Some apps will resist our efforts

    View Slide

  10. So what can we do?

    View Slide

  11. It’s About Tools

    View Slide

  12. It’s About Strategies

    View Slide

  13. WHAT DOES TESTABLE CODE
    LOOK LIKE?

    View Slide

  14. ✤ Single-purpose objects!
    ✤ Loose coupling!
    ✤ Consistency in architectural decisions

    View Slide

  15. View Slide

  16. ✤ Desired functionality clearer!
    ✤ Less methods to test!
    ✤ More likely to be easily extendable

    View Slide

  17. “Simple systems can display complex behaviour but complex
    systems can only display simple behaviour.”
    – Chris Hartjes

    View Slide

  18. View Slide

  19. “It's called the Onion Architecture and its main principal is
    this: organize the layers of your application code like the
    layers of an onion. Every layer of the onion is only coupled
    (or dependent upon) any layer deeper in the onion than
    itself.”
    –Kristopher Wilson
    http://kristopherwilson.com/2013/07/04/implementing-the-onion-
    architecture-in-php/

    View Slide

  20. View Slide

  21. View Slide

  22. “Inside every large program is a small program struggling to
    get out.”
    – Hoare’s Law of Large Programs

    View Slide

  23. “Your framework should be an implementation detail, not a
    giant killer squid, sucking the life out your application.”
    – Chris Hartjes

    View Slide

  24. View Slide

  25. The 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.”

    View Slide

  26. “Also known more commonly as!
    ‘passing in a parameter’”
    –Chris Hartjes

    View Slide

  27. Dependency Injection
    “Pass objects and their methods!
    other objects that are required.”

    View Slide

  28. 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');!
    }

    View Slide

  29. !
    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');!
    }

    View Slide

  30. !
    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');!
    }

    View Slide

  31. Test Doubles
    ✤ create copies of existing objects for testing purposes!
    ✤ essential for effective unit testing

    View Slide

  32. ✤ Database connections!
    ✤ Web services!
    ✤ File system operations!
    ✤ Other objects our test-under-code needs to use

    View Slide

  33. ✤ PHPUnit’s built-in support!
    ✤ Mockery !
    ✤ Prophecy

    View Slide

  34. 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');!
    }

    View Slide

  35. class GrumpyAclTest extends \PHPUnit_Framework_TestCase!
    {!
    ! public function testAdminPurgeAccessAllowed()!
    ! {!
    ! ! $mockRequest = $this->getMockBuilder('\Grumpy\Controller\Request')!
    ! ! ! ->disableOriginalConstructor()!
    ! ! ! ->getMock();!
    ! ! $mockRequest->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'!
    ! ! );!
    ! }!
    }

    View Slide

  36. Protected and Private Methods
    “Protected and !
    private methods and!
    attributes are difficult!
    to test properly.”

    View Slide

  37. Etsy’s PHPUnit Extensions!
    https://github.com/etsy/phpunit-extensions
    Uses annotations to flag!
    methods you wish to test

    View Slide

  38. class ObjectWithPrivate {!
    !
    ! private function myInaccessiblePrivateMethod()!
    ! {!
    ! ! return 'inaccessible';!
    ! }!
    !
    ! /** @accessibleForTesting */!
    ! private function myAccessiblePrivateMethod() {!
    ! ! return 'accessible';!
    ! }!
    }!

    View Slide

  39. 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()!
    ! ! );!
    ! }!
    }!

    View Slide

  40. PHP’S Reflection API!
    !
    http://www.gpug.ca/2012/06/02/testing-protected-methods-
    with-phpunit/

    View Slide

  41. class Foo!
    {!
    ! protected $_message;!
    !
    ! protected function _bar()!
    ! {!
    ! ! $this->_message = 'WRITE TESTS OR I CUT YOU';!
    ! }!
    }!

    View Slide

  42. 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"!
    ! ! );!
    !
    ! }!
    }!

    View Slide

  43. “I suggest never testing private or protected methods
    directly. Instead, test public methods and use code coverage
    information to make sure they are executed.”
    –Chris Hartjes

    View Slide

  44. Databases In Unit Tests
    “I will just create!
    a database for!
    testing purposes!”

    View Slide

  45. View Slide

  46. “If you use a database in your unit tests, you are simply
    testing that your database works. It better work or else you
    are screwed.”
    –Chris Hartjes

    View Slide

  47. View Slide

  48. 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;!
    ! }!
    }!

    View Slide

  49. 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));!
    !
    ! ! // Actual test!
    ! ! (…)!
    ! }!
    }!

    View Slide

  50. class BarTest extends PHPUnit_Framework_Testcase!
    {!
    ! public function testGetBazById()!
    ! {!
    ! ! (…)!
    ! ! // Test setup above!
    ! ! !
    ! ! $testBar = new Bar();!
    ! ! $testBar->setDb($mockDb);!
    ! ! $testResults = $testBar->getBazById($bazId);!
    !
    ! ! $this->assertEquals(!
    ! ! ! $expectedResults,!
    ! ! ! $testResults,!
    ! ! ! 'Did not get expected baz result set'!
    ! ! );!
    ! }!
    }!

    View Slide

  51. API Calls

    View Slide

  52. !
    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();!
    ! }!
    }!

    View Slide

  53. 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);!
    !
    ! ! // Test follows!
    ! ! (…)!
    !
    ! }!
    }!

    View Slide

  54. class HipsterApiTest extends PHPUnit_Framework_Testcase!
    {!
    ! public function testGetBands()!
    ! {!
    ! ! (…)!
    ! ! // Test setup above!
    !
    ! ! $hipsterApiWrapper = new HipsterApiWrapper($mockHipsterApi);!
    ! ! $testData = $hipsterApiWrapper->getBands();!
    ! ! ! !
    ! ! $this->assertEquals(!
    ! ! ! $expectedData,!
    ! ! ! $testData,!
    ! ! ! 'Did not get expected getBands() result from HipsterApi'!
    ! ! );!
    ! }!
    }

    View Slide

  55. Environmental Concerns
    “Your app shouldn’t!
    care what environment!
    it runs in.”

    View Slide

  56. ✤ PHPDotEnv (https://github.com/vlucas/phpdotenv)!
    ✤ Old-school INI files in their own VCS repo

    View Slide

  57. “Where did you learn!
    about all this stuff?!?”!

    View Slide

  58. View Slide

  59. View Slide

  60. http://grumpy-testing.com

    View Slide

  61. http://grumpy-phpunit.com

    View Slide

  62. http://grumpy-learning.com

    View Slide

  63. THANK YOU!
    ✤ Twitter -> @grmpyprogrammer!
    ✤ Email -> [email protected]!
    ✤ IRC -> #phpmentoring on Freenode!
    ✤ https://joind.in/10440

    View Slide