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

Testing Online Crazy Glue

Testing Online Crazy Glue

Talk on building testable PHP applications that I gave at Øredev 2012

Chris Hartjes

November 07, 2012
Tweet

More Decks by Chris Hartjes

Other Decks in Programming

Transcript

  1. Testing Online
    Crazy Glue
    Chris Hartjes
    Øredev 2012 - November 7, 2012
    @grmpyprogrammer

    View full-size slide

  2. Text
    Story
    Time

    View full-size slide

  3. WHY DO WE TEST?
    Because programming
    is hard

    View full-size slide

  4. WHY DO WE TEST?

    View full-size slide

  5. A HUGE TOPIC

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. UNCOMFORTABLE TRUTHS
    Testing is good
    Testable applications
    are better

    View full-size slide

  9. SO WHAT CAN WE DO?

    View full-size slide

  10. IT’S ABOUT TOOLS

    View full-size slide

  11. IT’S ABOUT STRATEGIES

    View full-size slide

  12. AUTOMATION IS KEY

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  19. ARCHITECTURE
    “One of the great bugaboos of
    software applications over the years
    has been infiltration of business
    logic into the user interface code.”
    -- Alistair Cockburn
    http://alistair.cockburn.us/Hexagonal+architecture

    View full-size 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 full-size slide

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

    View full-size 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

  26. MOCK OBJECTS
    Database connections
    Web services
    File system operations

    View full-size 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 full-size slide

  28. HOW DO WE TEST THIS?
    class GrumpyAclTest extends \PHPUnit_Framework_TestCase
    {
    ! public function testAdminPurgeAccessAllowed()
    ! {
    $testUri = '/account/purge';
    ! ! $mockRequest = $this->getMockBuilder('\Grumpy\Controller\Request')
    ! ! ! ->disableOriginalConstructor()
    ! ! ! ->getMock();
    ! ! $mockController->expects($this->once))
    ! ! ! ->method('getUri')
    ! ! ! ->will($this->returnValue($testUri));
    ! ! $testUserLevel = 'admin';
    ! ! $acl = new \Grumpy\Acl($mockRequest, $testUserLevel);
    ! ! $this->assertTrue(
    ! ! ! $acl->accessAllowed(),
    ! ! ! 'admin user should have access to purge accounts'
    ! ! );
    ! }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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($expectedResults));
    ...!
    !
    ! }
    }

    View full-size slide

  40. HOW DO YOU TEST THIS?
    class BarTest extends PHPUnit_Framework_Testcase
    {
    ! public function testGetBazById()
    ! {
    ! ! ...
    ! ! $testBar = new Bar();
    ! ! $testBar->setDb($mockDb);
    ! ! $testResults = $testBar->getBazById($bazId);
    ! ! $this->assertEquals(
    ! ! ! $expectedResults,
    ! ! ! $testResults,
    ! ! ! 'Did not get expected baz result set'
    ! ! );
    ! }
    }

    View full-size slide

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

    View full-size slide

  42. 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 full-size slide

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

    View full-size slide

  44. HOW DO WE TEST THIS?
    class HipsterApiTest extends PHPUnit_Framework_Testcase
    {
    ! public function testGetBands()
    ! {
    ! ! ...
    ! ! $hipsterApiWrapper =
    new HipsterApiWrapper($mockHipsterApi);
    ! ! $testData = $hipsterApiWrapper->getBands();
    ! ! !
    ! ! $this->assertEquals(
    ! ! ! $expectedData,
    ! ! ! $testData,
    ! ! ! 'Did not get expected getBands() result from HipsterApi'
    ! ! );
    ! }
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  47. RESOURCES
    PHPUnit
    http:/
    /phpunit.de

    View full-size slide

  48. RESOURCES
    Behat
    http:/
    /behat.org

    View full-size slide

  49. 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 full-size slide

  50. RESOURCES
    The people
    sitting next
    to you

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  53. THANK YOU!
    @grmpyprogrammer
    http:/
    /www.littlehart.net/atthekeyboard
    https:/
    /speakerdeck.com/grumpycanuck/testing-
    online-crazy-glue

    View full-size slide