Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Text Story Time

Slide 3

Slide 3 text

WHY DO WE TEST? Because programming is hard

Slide 4

Slide 4 text

WHY DO WE TEST?

Slide 5

Slide 5 text

A HUGE TOPIC

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

UNCOMFORTABLE TRUTHS Some of this will not make sense to you

Slide 8

Slide 8 text

UNCOMFORTABLE TRUTHS Some applications will resist all attempts to test

Slide 9

Slide 9 text

UNCOMFORTABLE TRUTHS Testing is good Testable applications are better

Slide 10

Slide 10 text

SO WHAT CAN WE DO?

Slide 11

Slide 11 text

IT’S ABOUT TOOLS

Slide 12

Slide 12 text

IT’S ABOUT STRATEGIES

Slide 13

Slide 13 text

AUTOMATION IS KEY

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

DEPENDENCY INJECTION _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'); }

Slide 24

Slide 24 text

DEPENDENCY INJECTION _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'); }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

MOCK OBJECTS Database connections Web services File system operations

Slide 27

Slide 27 text

HOW DO WE TEST 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'); }

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

HOW DO YOU TEST THIS? _call('/api/bands', $this->_apiKey); ! } } class HipsterApiWrapper { ! public function __construct($hipsterApi) ! { ! ! $this->_hipsterApi = $hipsterApi; ! } ! public function getBands() ! { ! ! return $this->_hipsterApi->getBands(); ! } }

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

RESOURCES PHPUnit http:/ /phpunit.de

Slide 46

Slide 46 text

RESOURCES Behat http:/ /behat.org

Slide 47

Slide 47 text

RESOURCES The people sitting next to you

Slide 48

Slide 48 text

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"

Slide 49

Slide 49 text

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