Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
Building Testable PHP Applications
Chris Hartjes
March 03, 2013
Technology
4
460
Building Testable PHP Applications
Slides from Midwest PHP 2013
Chris Hartjes
March 03, 2013
Tweet
Share
More Decks by Chris Hartjes
See All by Chris Hartjes
Confessions of a not-so-accidental leader
grumpycanuck
0
93
Lessons Learned From 10 Years Of Testing
grumpycanuck
3
99
Learn To Test Like A Grumpy Programmer
grumpycanuck
0
170
Time Management For Grumpy Programmers
grumpycanuck
0
140
Learn To Test Like A Grumpy Programmer
grumpycanuck
1
180
Learn To Test Like A Grumpy Programmer
grumpycanuck
2
160
Grumpy Testing Patterns
grumpycanuck
1
720
Embrace Your Inner Grumpy: Metatesting in 2016
grumpycanuck
0
72
Smelly Tests
grumpycanuck
0
40
Other Decks in Technology
See All in Technology
A3-1 IBM Championが本音で語る「IBM Cloud」
kolinz
0
310
ECS on EC2 で Auto Scaling やってみる!
sayjoy
1
250
2022 COSCUP - GKE Backend Cluster 除雷分享
brentchang
0
120
DeFiChain Tech Talk - DFI Uniswap Staking, DeFi Options & DeFi Meta Chain
uzyn
0
120
合同IT企業説明会から学ぶエンジニア向けの広報戦略
nagutabby
1
240
20220803投資先CXO候補者向け 会社紹介資料_合同会社BLUEPRINT
hik
0
360
Simplify Cloud Native Security with Trivy
knqyf263
0
690
テスト自動化を最速で軌道に乗せるために
nozomiito
0
140
hey BOOK
heyinc
26
290k
疎ベクトル検索と密ベクトル検索: 第68回 Machine Learning 15minutes! Broadcast
keyakkie
1
250
フィンテック養成勉強会#24
finengine
0
340
データをコネコネ!メール配信用データ生成の仕組み
kappezoro
0
120
Featured
See All Featured
A designer walks into a library…
pauljervisheath
196
16k
Side Projects
sachag
450
37k
GraphQLとの向き合い方2022年版
quramy
16
8.5k
Support Driven Design
roundedbygravity
87
8.6k
Visualization
eitanlees
125
12k
Bootstrapping a Software Product
garrettdimon
296
110k
Practical Orchestrator
shlominoach
178
8.7k
Building Your Own Lightsaber
phodgson
95
4.7k
Web Components: a chance to create the future
zenorocha
303
40k
How GitHub (no longer) Works
holman
297
140k
Making the Leap to Tech Lead
cromwellryan
113
7.4k
CSS Pre-Processors: Stylus, Less & Sass
bermonpainter
349
27k
Transcript
Building Testable PHP Applications Chris Hartjes MidwestPHP 2013 - Mar.
3, 2013 @grmpyprogrammer
Text Story Time
WHY DO WE TEST? Because programming is hard
WHY DO WE TEST?
A HUGE TOPIC
UNCOMFORTABLE TRUTHS Some of this will not make sense to
you
UNCOMFORTABLE TRUTHS Some applications will resist all attempts to test
UNCOMFORTABLE TRUTHS Testing is good Testable applications are better
SO WHAT CAN WE DO?
IT’S ABOUT TOOLS
IT’S ABOUT STRATEGIES
AUTOMATION IS KEY
AUTOMATION IS KEY “Write a script that will run all
your tests before you go live”
AUTOMATION “Tell your version control system to run your tests
on commit or push”
AUTOMATION IS KEY http://jenkins-ci.org http://travis-ci.org
ARCHITECTURE “Simple systems can display complex behavior but complex systems
can only display simple behaviour”
ARCHITECTURE “Inside every great large application are many great small
applications”
ARCHITECTURE “Your framework is a detail, not the core of
your application.” -- Bob Martin
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. ”
DEPENDENCY INJECTION “Pass objects and their methods other objects and
function that are required for the task.”
DEPENDENCY INJECTION <?php 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'); }
DEPENDENCY INJECTION <?php 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'); }
DEPENDENCY INJECTION <?php 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'); }
MOCK OBJECTS “Mock objects allow you to test code in
proper isolation”
MOCK OBJECTS Database connections Web services File system operations
HOW DO WE TEST THIS? <?php 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'); }
HOW DO WE TEST THIS? <?php class GrumpyAclTest extends \PHPUnit_Framework_TestCase
{ ! public function testAdminPurgeAccessAllowed() ! { ! ! $mockRequest = $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' ! ! ); ! } }
HOW DO WE TEST THIS? “Protected and private methods and
attributes are difficult to test properly”
METHODS? Etsy’s PHPUnit Extensions https:/ /github.com/etsy/phpunit-extensions Uses annotations to flag
methods you wish to test
METHODS? class ObjectWithPrivate { ! private function myInaccessiblePrivateMethod() ! {
! ! return 'inaccessible'; ! } ! /** @accessibleForTesting */ ! private function myAccessiblePrivateMethod() { ! ! return 'accessible'; ! } }
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() ! ! ); ! } }
METHODS? PHP’S Reflection API http:/ /www.gpug.ca/2012/06/02/testing- protected-methods-with-phpunit/
METHODS? class Foo { ! protected $_message; ! protected function
_bar() ! { ! ! $this->_message = 'WRITE TESTS OR I CUT YOU'; ! } }
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" ! ! ); ! } }
ATTRIBUTES? PHP’S Reflection API PHPUnit lets you check attribute values
but not set them
HOW DO YOU TEST THIS? “If your unit test actually
uses the database, you are doing it wrong”
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; ! } }
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' ! ! ); ! } }
HOW DO YOU TEST THIS? “API calls should be done
via wrapper methods”
HOW DO YOU TEST THIS? <?php 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(); ! } }
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' ! ! ); ! } }
ENVIRONMENTS “Your app shouldn’t care what environment it runs in.”
ENVIRONMENTS “Keep config files for each environment under version control”
https:/ /github.com/flogic/ whiskey_disk
WHAT NOT TO DO
WHAT NOT TO DO “Avoid the use of static method
calls”
WHAT NOT TO DO <?php class Presentation { public function
speak() { $rant = Grumpy::getOpinion(); } }
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
WHAT NOT TO DO “Avoid creating new non-core objects unless
it’s a factory”
WHAT NOT TO DO <?php class Presentation { public function
convert($name) { $slides = new SlideDriver(); $slides->toPdf($name); } }
WHAT NOT TO DO 1. Learn dependency injection 2. Create
version of your object 3. Inject the dependency
WHAT NOT TO DO “Be careful with chaining methods”
WHAT NOT TO DO <?php class Presentation { public function
munge($frozbit) { $widgets = $frozbit ->what() ->is() ->this() ->i() ->dont() ->even(); } }
WHAT NOT TO DO 1. Reduce the number of chains
2. Reduce the amount of work
RESOURCES The Grumpy Programmer’s Guide to Building Testable PHP Applications
http:/ /grumpy-testing.com
RESOURCES The Grumpy Programmer’s PHPUnit Cookbook http:/ /grumpy-phpunit.com
RESOURCES The people sitting next to you
THANK YOU! @grmpyprogrammer http:/ /www.littlehart.net/atthekeyboard https:/ /joind.in/8233