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
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
Chris Hartjes
March 03, 2013
Technology
4
640
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
240
Lessons Learned From 10 Years Of Testing
grumpycanuck
4
140
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
270
Learn To Test Like A Grumpy Programmer
grumpycanuck
2
210
Grumpy Testing Patterns
grumpycanuck
1
1k
Embrace Your Inner Grumpy: Metatesting in 2016
grumpycanuck
0
150
Smelly Tests
grumpycanuck
0
100
Other Decks in Technology
See All in Technology
LINEアプリ開発のための Claude Code活用基盤の構築
lycorptech_jp
PRO
1
1.3k
Vertex AI Agent Engine で学ぶ「記憶」の設計
tkikuchi
0
120
バクラクにおける Document Understanding の挑戦:書類の「読取」から「意思決定」へ / document-understanding-in-bakuraku-2026
yuya4
0
190
「使いにくい」も「運用疲れ」も卒業する UIデザイナーとエンジニアが創る持続可能な内製開発
nrinetcom
PRO
1
770
メタデータ同期に潜んでいた問題 〜 Cache Stampede 時の Cycle Wait を⾒つけた話
lycorptech_jp
PRO
0
130
Secure Boot 2026 - Aggiornamento dei certificati UEFI e piano di adozione in azienda
memiug
0
130
Raspberry Pi AI HAT+ 2 介紹(#49)
piepie_tw
PRO
0
120
LINEヤフーにおけるAI駆動開発組織のプロデュース施策
lycorptech_jp
PRO
0
340
AIに視覚を与えモバイルアプリケーション開発をより円滑に行う
lycorptech_jp
PRO
1
720
どこで打鍵するのが良い? IaCの実行基盤選定について
nrinetcom
PRO
2
110
Microsoft Fabric のワークスペースと容量の設計原則
ryomaru0825
2
220
Oracle Cloud Infrastructure:2026年2月度サービス・アップデート
oracle4engineer
PRO
0
140
Featured
See All Featured
Building Applications with DynamoDB
mza
96
6.9k
ラッコキーワード サービス紹介資料
rakko
1
2.5M
Reality Check: Gamification 10 Years Later
codingconduct
0
2k
Avoiding the “Bad Training, Faster” Trap in the Age of AI
tmiket
0
95
Design in an AI World
tapps
0
160
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.2k
Making Projects Easy
brettharned
120
6.6k
Claude Code のすすめ
schroneko
67
220k
Understanding Cognitive Biases in Performance Measurement
bluesmoon
32
2.8k
Claude Code どこまでも/ Claude Code Everywhere
nwiizo
63
53k
Exploring anti-patterns in Rails
aemeredith
2
280
ReactJS: Keep Simple. Everything can be a component!
pedronauck
666
130k
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