Slide 1

Slide 1 text

January 18, 2014 Understanding The Helter Skelter World of Building Testable PHP Applications by Chris Hartjes - SkiPHP 2014

Slide 2

Slide 2 text

01 Who is the guy?

Slide 3

Slide 3 text

01

Slide 4

Slide 4 text

2002

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

Some apps will resist our efforts

Slide 10

Slide 10 text

So what can we do?

Slide 11

Slide 11 text

It’s About Tools

Slide 12

Slide 12 text

It’s About Strategies

Slide 13

Slide 13 text

WHAT DOES TESTABLE CODE LOOK LIKE?

Slide 14

Slide 14 text

✤ Single-purpose objects! ✤ Loose coupling! ✤ Consistency in architectural decisions

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

✤ Desired functionality clearer! ✤ Less methods to test! ✤ More likely to be easily extendable

Slide 17

Slide 17 text

“Simple systems can display complex behaviour but complex systems can only display simple behaviour.” – Chris Hartjes

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

“It's called the Onion Architecture and its main principal is this: organize the layers of your application code like the layers of an onion. Every layer of the onion is only coupled (or dependent upon) any layer deeper in the onion than itself.” –Kristopher Wilson http://kristopherwilson.com/2013/07/04/implementing-the-onion- architecture-in-php/

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

“Inside every large program is a small program struggling to get out.” – Hoare’s Law of Large Programs

Slide 23

Slide 23 text

“Your framework should be an implementation detail, not a giant killer squid, sucking the life out your application.” – Chris Hartjes

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

The 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.”

Slide 26

Slide 26 text

“Also known more commonly as! ‘passing in a parameter’” –Chris Hartjes

Slide 27

Slide 27 text

Dependency Injection “Pass objects and their methods! other objects that are required.”

Slide 28

Slide 28 text

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');! }

Slide 29

Slide 29 text

_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');! }

Slide 30

Slide 30 text

_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');! }

Slide 31

Slide 31 text

Test Doubles ✤ create copies of existing objects for testing purposes! ✤ essential for effective unit testing

Slide 32

Slide 32 text

✤ Database connections! ✤ Web services! ✤ File system operations! ✤ Other objects our test-under-code needs to use

Slide 33

Slide 33 text

✤ PHPUnit’s built-in support! ✤ Mockery ! ✤ Prophecy

Slide 34

Slide 34 text

_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');! }

Slide 35

Slide 35 text

getMockBuilder('\Grumpy\Controller\Request')! ! ! ! ->disableOriginalConstructor()! ! ! ! ->getMock();! ! ! $mockRequest->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'! ! ! );! ! }! }

Slide 36

Slide 36 text

Protected and Private Methods “Protected and ! private methods and! attributes are difficult! to test properly.”

Slide 37

Slide 37 text

Etsy’s PHPUnit Extensions! https://github.com/etsy/phpunit-extensions Uses annotations to flag! methods you wish to test

Slide 38

Slide 38 text

class ObjectWithPrivate {! ! ! private function myInaccessiblePrivateMethod()! ! {! ! ! return 'inaccessible';! ! }! ! ! /** @accessibleForTesting */! ! private function myAccessiblePrivateMethod() {! ! ! return 'accessible';! ! }! }!

Slide 39

Slide 39 text

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()! ! ! );! ! }! }!

Slide 40

Slide 40 text

PHP’S Reflection API! ! http://www.gpug.ca/2012/06/02/testing-protected-methods- with-phpunit/

Slide 41

Slide 41 text

class Foo! {! ! protected $_message;! ! ! protected function _bar()! ! {! ! ! $this->_message = 'WRITE TESTS OR I CUT YOU';! ! }! }!

Slide 42

Slide 42 text

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"! ! ! );! ! ! }! }!

Slide 43

Slide 43 text

“I suggest never testing private or protected methods directly. Instead, test public methods and use code coverage information to make sure they are executed.” –Chris Hartjes

Slide 44

Slide 44 text

Databases In Unit Tests “I will just create! a database for! testing purposes!”

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

“If you use a database in your unit tests, you are simply testing that your database works. It better work or else you are screwed.” –Chris Hartjes

Slide 47

Slide 47 text

No content

Slide 48

Slide 48 text

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;! ! }! }!

Slide 49

Slide 49 text

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));! ! ! ! // Actual test! ! ! (…)! ! }! }!

Slide 50

Slide 50 text

class BarTest extends PHPUnit_Framework_Testcase! {! ! public function testGetBazById()! ! {! ! ! (…)! ! ! // Test setup above! ! ! ! ! ! $testBar = new Bar();! ! ! $testBar->setDb($mockDb);! ! ! $testResults = $testBar->getBazById($bazId);! ! ! ! $this->assertEquals(! ! ! ! $expectedResults,! ! ! ! $testResults,! ! ! ! 'Did not get expected baz result set'! ! ! );! ! }! }!

Slide 51

Slide 51 text

API Calls

Slide 52

Slide 52 text

_call('/api/bands', $this->_apiKey);! ! }! }! ! class HipsterApiWrapper! {! ! public function __construct($hipsterApi)! ! {! ! ! $this->_hipsterApi = $hipsterApi;! ! }! ! ! public function getBands()! ! {! ! ! return $this->_hipsterApi->getBands();! ! }! }!

Slide 53

Slide 53 text

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);! ! ! ! // Test follows! ! ! (…)! ! ! }! }!

Slide 54

Slide 54 text

class HipsterApiTest extends PHPUnit_Framework_Testcase! {! ! public function testGetBands()! ! {! ! ! (…)! ! ! // Test setup above! ! ! ! $hipsterApiWrapper = new HipsterApiWrapper($mockHipsterApi);! ! ! $testData = $hipsterApiWrapper->getBands();! ! ! ! ! ! ! $this->assertEquals(! ! ! ! $expectedData,! ! ! ! $testData,! ! ! ! 'Did not get expected getBands() result from HipsterApi'! ! ! );! ! }! }

Slide 55

Slide 55 text

Environmental Concerns “Your app shouldn’t! care what environment! it runs in.”

Slide 56

Slide 56 text

✤ PHPDotEnv (https://github.com/vlucas/phpdotenv)! ✤ Old-school INI files in their own VCS repo

Slide 57

Slide 57 text

“Where did you learn! about all this stuff?!?”!

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

http://grumpy-testing.com

Slide 61

Slide 61 text

http://grumpy-phpunit.com

Slide 62

Slide 62 text

http://grumpy-learning.com

Slide 63

Slide 63 text

THANK YOU! ✤ Twitter -> @grmpyprogrammer! ✤ Email -> [email protected]! ✤ IRC -> #phpmentoring on Freenode! ✤ https://joind.in/10440