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. Building Testable
    PHP Applications
    Chris Hartjes
    MidwestPHP 2013 - Mar. 3, 2013
    @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. UNCOMFORTABLE TRUTHS
    Some of this
    will not make
    sense to you

    View Slide

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

    View Slide

  8. UNCOMFORTABLE TRUTHS
    Testing is good
    Testable applications
    are better

    View Slide

  9. SO WHAT CAN WE DO?

    View Slide

  10. IT’S ABOUT TOOLS

    View Slide

  11. IT’S ABOUT STRATEGIES

    View Slide

  12. AUTOMATION IS KEY

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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.

    View Slide

  20. DEPENDENCY INJECTION
    “Pass objects and their methods
    other objects and function that are
    required for the task.”

    View Slide

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

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

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

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

    View Slide

  25. MOCK OBJECTS
    Database connections
    Web services
    File system operations

    View Slide

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

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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

    View Slide

  44. WHAT NOT TO DO

    View Slide

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

    View Slide

  46. WHAT NOT TO DO
    class Presentation
    {
    public function speak()
    {
    $rant = Grumpy::getOpinion();
    }
    }

    View Slide

  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

    View Slide

  48. WHAT NOT TO DO
    “Avoid creating
    new non-core objects
    unless it’s a factory”

    View Slide

  49. WHAT NOT TO DO
    class Presentation
    {
    public function convert($name)
    {
    $slides = new SlideDriver();
    $slides->toPdf($name);
    }
    }

    View Slide

  50. WHAT NOT TO DO
    1. Learn dependency injection
    2. Create version of your object
    3. Inject the dependency

    View Slide

  51. WHAT NOT TO DO
    “Be careful with
    chaining methods”

    View Slide

  52. WHAT NOT TO DO
    class Presentation
    {
    public function munge($frozbit)
    {
    $widgets = $frozbit
    ->what()
    ->is()
    ->this()
    ->i()
    ->dont()
    ->even();
    }
    }

    View Slide

  53. WHAT NOT TO DO
    1. Reduce the number of chains
    2. Reduce the amount of work

    View Slide

  54. RESOURCES
    The Grumpy Programmer’s
    Guide to Building
    Testable PHP Applications
    http:/
    /grumpy-testing.com

    View Slide

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

    View Slide

  56. RESOURCES
    The people
    sitting next
    to you

    View Slide

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

    View Slide