Slide 1

Slide 1 text

Building Testable PHP Applications Chris Hartjes MidwestPHP 2013 - Mar. 3, 2013 @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

UNCOMFORTABLE TRUTHS Some of this will not make sense to you

Slide 7

Slide 7 text

UNCOMFORTABLE TRUTHS Some applications will resist all attempts to test

Slide 8

Slide 8 text

UNCOMFORTABLE TRUTHS Testing is good Testable applications are better

Slide 9

Slide 9 text

SO WHAT CAN WE DO?

Slide 10

Slide 10 text

IT’S ABOUT TOOLS

Slide 11

Slide 11 text

IT’S ABOUT STRATEGIES

Slide 12

Slide 12 text

AUTOMATION IS KEY

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

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

Slide 20 text

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

Slide 21

Slide 21 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 22

Slide 22 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 23

Slide 23 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 24

Slide 24 text

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

Slide 25

Slide 25 text

MOCK OBJECTS Database connections Web services File system operations

Slide 26

Slide 26 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 27

Slide 27 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 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 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 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 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 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 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 38

Slide 38 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 39

Slide 39 text

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

Slide 40

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

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

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

WHAT NOT TO DO

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

WHAT NOT TO DO

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

WHAT NOT TO DO toPdf($name); } }

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

WHAT NOT TO DO “Be careful with chaining methods”

Slide 52

Slide 52 text

WHAT NOT TO DO what() ->is() ->this() ->i() ->dont() ->even(); } }

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

RESOURCES The people sitting next to you

Slide 57

Slide 57 text

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