Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Building Testable PHP Applications
Search
Chris Hartjes
March 03, 2013
Technology
4
620
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
210
Lessons Learned From 10 Years Of Testing
grumpycanuck
4
130
Learn To Test Like A Grumpy Programmer
grumpycanuck
0
240
Time Management For Grumpy Programmers
grumpycanuck
0
210
Learn To Test Like A Grumpy Programmer
grumpycanuck
1
240
Learn To Test Like A Grumpy Programmer
grumpycanuck
2
200
Grumpy Testing Patterns
grumpycanuck
1
960
Embrace Your Inner Grumpy: Metatesting in 2016
grumpycanuck
0
140
Smelly Tests
grumpycanuck
0
92
Other Decks in Technology
See All in Technology
意志の力が9割。アニメから学ぶAI時代のこれから。
endohizumi
1
100
PL/pgSQLの基本と使い所
tameguro
2
230
Agent Development Kitで始める生成 AI エージェント実践開発
danishi
0
160
o11yツールを乗り換えた話
tak0x00
2
1.6k
✨敗北解法コレクション✨〜Expertだった頃に足りなかった知識と技術〜
nanachi
1
760
Instant Apps Eulogy
cyrilmottier
1
120
Foundation Model × VisionKit で実現するローカル OCR
sansantech
PRO
1
400
【OptimizationNight】数理最適化のラストワンマイルとしてのUIUX
brainpadpr
2
520
Exadata Database Service on Dedicated Infrastructure セキュリティ、ネットワーク、および管理について
oracle4engineer
PRO
0
290
Google Cloud で学ぶデータエンジニアリング入門 2025年版 #GoogleCloudNext / 20250805
kazaneya
PRO
24
5.9k
九州の人に知ってもらいたいGISスポット / gis spot in kyushu 2025
sakaik
0
180
オブザーバビリティ文化を組織に浸透させるには / install observability culture
mackerelio
0
150
Featured
See All Featured
Faster Mobile Websites
deanohume
309
31k
The Illustrated Children's Guide to Kubernetes
chrisshort
48
50k
KATA
mclloyd
32
14k
Performance Is Good for Brains [We Love Speed 2024]
tammyeverts
10
1k
GitHub's CSS Performance
jonrohan
1031
460k
個人開発の失敗を避けるイケてる考え方 / tips for indie hackers
panda_program
110
20k
Scaling GitHub
holman
462
140k
Building a Scalable Design System with Sketch
lauravandoore
462
33k
Code Review Best Practice
trishagee
69
19k
ピンチをチャンスに:未来をつくるプロダクトロードマップ #pmconf2020
aki_iinuma
126
53k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
656
60k
A designer walks into a library…
pauljervisheath
207
24k
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