$30 off During Our Annual Pro Sale. View Details »
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
230
Lessons Learned From 10 Years Of Testing
grumpycanuck
4
130
Learn To Test Like A Grumpy Programmer
grumpycanuck
0
250
Time Management For Grumpy Programmers
grumpycanuck
0
220
Learn To Test Like A Grumpy Programmer
grumpycanuck
1
260
Learn To Test Like A Grumpy Programmer
grumpycanuck
2
210
Grumpy Testing Patterns
grumpycanuck
1
990
Embrace Your Inner Grumpy: Metatesting in 2016
grumpycanuck
0
150
Smelly Tests
grumpycanuck
0
99
Other Decks in Technology
See All in Technology
AI with TiDD
shiraji
1
280
20251218_AIを活用した開発生産性向上の全社的な取り組みの進め方について / How to proceed with company-wide initiatives to improve development productivity using AI
yayoi_dd
0
660
AWSに革命を起こすかもしれない新サービス・アップデートについてのお話
yama3133
0
500
特別捜査官等研修会
nomizone
0
570
Oracle Database@Azure:サービス概要のご紹介
oracle4engineer
PRO
2
200
202512_AIoT.pdf
iotcomjpadmin
0
140
障害対応訓練、その前に
coconala_engineer
0
200
【開発を止めるな】機能追加と並行して進めるアーキテクチャ改善/Keep Shipping: Architecture Improvements Without Pausing Dev
bitkey
PRO
1
130
ペアーズにおけるAIエージェント 基盤とText to SQLツールの紹介
hisamouna
2
1.7k
AI駆動開発ライフサイクル(AI-DLC)の始め方
ryansbcho79
0
180
AgentCoreとStrandsで社内d払いナレッジボットを作った話
motojimayu
1
940
フルカイテン株式会社 エンジニア向け採用資料
fullkaiten
0
9.9k
Featured
See All Featured
The B2B funnel & how to create a winning content strategy
katarinadahlin
PRO
0
190
Applied NLP in the Age of Generative AI
inesmontani
PRO
3
2k
Git: the NoSQL Database
bkeepers
PRO
432
66k
The Power of CSS Pseudo Elements
geoffreycrofte
80
6.1k
The untapped power of vector embeddings
frankvandijk
1
1.5k
The innovator’s Mindset - Leading Through an Era of Exponential Change - McGill University 2025
jdejongh
PRO
1
69
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.3k
The Language of Interfaces
destraynor
162
25k
Unlocking the hidden potential of vector embeddings in international SEO
frankvandijk
0
130
Taking LLMs out of the black box: A practical guide to human-in-the-loop distillation
inesmontani
PRO
3
2k
Evolving SEO for Evolving Search Engines
ryanjones
0
73
Creating an realtime collaboration tool: Agile Flush - .NET Oxford
marcduiker
35
2.3k
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