$30 off During Our Annual Pro Sale. View Details »

Building Testable PHP Applications

Building Testable PHP Applications

Slides from the presentation "Building Testable PHP Applications" given at Lone Star PHP 2012

Chris Hartjes

June 29, 2012
Tweet

More Decks by Chris Hartjes

Other Decks in Programming

Transcript

  1. Building Testable
    PHP Applications
    Chris Hartjes
    Lone Star PHP - June 29, 2012
    @grmpyprogrammer

    View Slide

  2. Text
    Story
    Time

    View Slide

  3. WHY DO WE TEST?
    Because programming
    is hard

    View Slide

  4. WHY DO WE TEST?

    View Slide

  5. A HUGE TOPIC

    View Slide

  6. WE ARE TALKING ABOUT
    PHPUnit
    PHP 5.3+
    My way of doing things

    View Slide

  7. UNCOMFORTABLE TRUTHS
    Some of this
    will not make
    sense to you

    View Slide

  8. UNCOMFORTABLE TRUTHS
    Some applications
    will resist all
    attempts to test

    View Slide

  9. UNCOMFORTABLE TRUTHS
    Testing is good
    Testable applications
    are better

    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. AUTOMATION IS KEY

    View Slide

  14. AUTOMATION IS KEY
    “Write a script that will
    run all your tests before
    you go live”

    View Slide

  15. AUTOMATION IS KEY
    http://jenkins-ci.org
    http://travis-ci.org

    View Slide

  16. AUTOMATION
    “Tell your version
    control system to
    run your tests
    on commit or push”

    View Slide

  17. ARCHITECTURE
    “Simple systems can
    display complex behavior
    but complex systems can
    only display simple
    behavior”

    View Slide

  18. ARCHITECTURE
    “Inside every great
    large application are
    many great small
    applications”

    View Slide

  19. ARCHITECTURE
    “Your framework
    is a detail, not
    the core of your
    application.”
    -- Bob Martin

    View Slide

  20. 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

  21. DEPENDENCY INJECTION
    “Pass objects functions, and methods
    other objects and function that are
    required for the task.”

    View Slide

  22. DEPENDENCY INJECTION
    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

  23. DEPENDENCY INJECTION
    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

  24. DEPENDENCY INJECTION
    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

  25. MOCK OBJECTS
    “Mock objects allow you
    to test code in proper
    isolation”

    View Slide

  26. MOCK OBJECTS
    Database connections
    Web services
    File system operations

    View Slide

  27. HOW DO WE TEST THIS?
    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

  28. HOW DO WE TEST THIS?
    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'
    ! ! );
    ! }
    }

    View Slide

  29. HOW DO WE TEST THIS?
    “Protected and
    private methods and
    attributes are difficult
    to test properly”

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. ATTRIBUTES?
    PHP’S Reflection API
    PHPUnit lets you check attribute
    values but not set them

    View Slide

  37. HOW DO YOU TEST THIS?
    “If your unit test
    actually uses the
    database, you are
    doing it wrong”

    View Slide

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

    View Slide

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

    View Slide

  40. HOW DO YOU TEST THIS?
    “API calls should
    be done via
    wrapper methods”

    View Slide

  41. HOW DO YOU TEST THIS?
    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

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

    View Slide

  43. ENVIRONMENTS
    “Your app shouldn’t
    care what environment
    it runs in.”

    View Slide

  44. ENVIRONMENTS
    “Keep config files
    for each environment
    under version control”
    https:/
    /github.com/flogic/
    whiskey_disk

    View Slide

  45. RESOURCES
    PHPUnit
    http:/
    /phpunit.de

    View Slide

  46. RESOURCES
    Behat
    http:/
    /behat.org

    View Slide

  47. RESOURCES
    The people
    sitting next
    to you

    View Slide

  48. BEHAT
    Feature: Do a Google search
    In order to find pages about Behat
    As a user
    I want to be able to use google.com to locate search results
    @javascript
    Scenario: I search for Behat
    Given I fill in "Behat github" for "q"
    When I press "Google Search"
    Then I should see "Trying to use Mink"

    View Slide

  49. THANK YOU!
    @grmpyprogrammer
    http:/
    /www.littlehart.net/atthekeyboard
    http:/
    /grumpy-testing.com
    http:/
    /devhell.info

    View Slide