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
  2. 01

  3. “Simple systems can display complex behaviour but complex systems can

    only display simple behaviour.” – Chris Hartjes
  4. “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/
  5. “Inside every large program is a small program struggling to

    get out.” – Hoare’s Law of Large Programs
  6. “Your framework should be an implementation detail, not a giant

    killer squid, sucking the life out your application.” – Chris Hartjes
  7. 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.”
  8. <?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');! }
  9. <?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');! }
  10. <?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');! }
  11. Test Doubles ✤ create copies of existing objects for testing

    purposes! ✤ essential for effective unit testing
  12. ✤ Database connections! ✤ Web services! ✤ File system operations!

    ✤ Other objects our test-under-code needs to use
  13. <?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');! }
  14. <?php! 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'! ! ! );! ! }! }
  15. Protected and Private Methods “Protected and ! private methods and!

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

    ! ! return 'inaccessible';! ! }! ! ! /** @accessibleForTesting */! ! private function myAccessiblePrivateMethod() {! ! ! return 'accessible';! ! }! }!
  17. 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()! ! ! );! ! }! }!
  18. class Foo! {! ! protected $_message;! ! ! protected function

    _bar()! ! {! ! ! $this->_message = 'WRITE TESTS OR I CUT YOU';! ! }! }!
  19. 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"! ! ! );! ! ! }! }!
  20. “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
  21. “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
  22. 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;! ! }! }!
  23. 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! ! ! (…)! ! }! }!
  24. 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'! ! ! );! ! }! }!
  25. <?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();! ! }! }!
  26. 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! ! ! (…)! ! ! }! }!
  27. 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'! ! ! );! ! }! }
  28. THANK YOU! ✤ Twitter -> @grmpyprogrammer! ✤ Email -> [email protected]!

    ✤ IRC -> #phpmentoring on Freenode! ✤ https://joind.in/10440