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 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
    with automation

    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 IS KEY
    “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 behaviour
    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. View Slide

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

  21. View Slide

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

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

    View Slide

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

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

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

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

    View Slide

  28. MOCK OBJECTS
    Database connections
    Web services
    File system operations

    View Slide

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

  30. 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 Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

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

    View Slide

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

    View Slide

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

  41. 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 Slide

  42. 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 Slide

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

    View Slide

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

  45. 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 Slide

  46. 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 Slide

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

    View Slide

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

    View Slide

  49. RESOURCES
    PHPUnit
    http:/
    /phpunit.de

    View Slide

  50. RESOURCES
    Behat
    http:/
    /behat.org

    View Slide

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

  52. RESOURCES
    The people
    sitting next
    to you

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide