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
550
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
150
Lessons Learned From 10 Years Of Testing
grumpycanuck
3
110
Learn To Test Like A Grumpy Programmer
grumpycanuck
0
200
Time Management For Grumpy Programmers
grumpycanuck
0
170
Learn To Test Like A Grumpy Programmer
grumpycanuck
1
210
Learn To Test Like A Grumpy Programmer
grumpycanuck
2
180
Grumpy Testing Patterns
grumpycanuck
1
830
Embrace Your Inner Grumpy: Metatesting in 2016
grumpycanuck
0
100
Smelly Tests
grumpycanuck
0
63
Other Decks in Technology
See All in Technology
本当のAWS基礎
toru_kubota
0
520
一生覚えておきたい「システム開発=コミュニケーション」〜初めての実務案件振り返りLT〜
maimyyym
0
140
エンジニア候補者向け資料2024.04.24.pdf
macloud
0
3.3k
私が trocco を推す理由
__allllllllez__
1
220
データベース02: データベースの概念
trycycle
0
160
【NW X Security JAWS#3】L3-4:AWS環境のIPv6移行に向けて知っておきたいこと
shotashiratori
0
150
開発パフォーマンスを最大化するための開発体制
ham0215
2
410
Gitlab本から学んだこと - そーだいなるプレイバック / gitlab-book
soudai
4
400
TechFeed Experts Night#27 〜 フロントエンドフレームワーク最前線 (Svelte)
baseballyama
1
500
プロンプトエンジニアリングでがんばらない-Agentic Workflow へ-近藤憲児
kenjikondobai
2
530
チームでロジカルシンキングに改めて向き合っている話 〜学習環境と実践⽅法〜
sansantech
PRO
3
2.5k
Azure Container Apps + Bicep 〜 こんな感じで運用しています
kaz29
2
470
Featured
See All Featured
Designing on Purpose - Digital PM Summit 2013
jponch
110
6.5k
Pencils Down: Stop Designing & Start Developing
hursman
117
11k
Large-scale JavaScript Application Architecture
addyosmani
504
110k
Infographics Made Easy
chrislema
238
18k
JavaScript: Past, Present, and Future - NDC Porto 2020
reverentgeek
40
4.4k
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
34
8.9k
How GitHub (no longer) Works
holman
304
140k
ParisWeb 2013: Learning to Love: Crash Course in Emotional UX Design
dotmariusz
104
6.6k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
244
20k
5 minutes of I Can Smell Your CMS
philhawksworth
199
19k
Building Your Own Lightsaber
phodgson
99
5.7k
4 Signs Your Business is Dying
shpigford
175
21k
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