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

Building Testable PHP Applications (php|tek 2013)

Building Testable PHP Applications (php|tek 2013)

Slides from my presentation at php|tek 2013

Chris Hartjes

May 16, 2013
Tweet

More Decks by Chris Hartjes

Other Decks in Programming

Transcript

  1. Building Testable
    PHP Applications
    Chris Hartjes
    php|tek 2013- May 16, 2013
    @grmpyprogrammer

    View Slide

  2. Text

    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
    behavior”

    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 and function that are
    required for the task.”

    View Slide

  24. DEPENDENCY INJECTION
    http://stackoverflow.com/questions/1638919/how-to-
    explain-dependency-injection-to-a-5-year-old/
    1638961#1638961

    View Slide

  25. DEPENDENCY INJECTION

    View Slide

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

    View Slide

  27. MOCK OBJECTS
    Database connections
    Web services
    File system operations

    View Slide

  28. HOW DO WE TEST THIS?

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

    View 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 Slide

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

    View 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 Slide

  43. HOW DO YOU 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

  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 Slide

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

    View Slide

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

    View Slide

  47. RESOURCES

    View Slide

  48. RESOURCES

    View Slide

  49. RESOURCES

    View Slide

  50. QUESTIONS?

    View Slide

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

    View Slide