Slide 1

Slide 1 text

Testing Online Crazy Glue Chris Hartjes Øredev 2012 - November 7, 2012 @grmpyprogrammer

Slide 2

Slide 2 text

Text Story Time

Slide 3

Slide 3 text

WHY DO WE TEST? Because programming is hard

Slide 4

Slide 4 text

WHY DO WE TEST?

Slide 5

Slide 5 text

A HUGE TOPIC

Slide 6

Slide 6 text

UNCOMFORTABLE TRUTHS Some of this will not make sense to you

Slide 7

Slide 7 text

UNCOMFORTABLE TRUTHS Some applications will resist all attempts to test with automation

Slide 8

Slide 8 text

UNCOMFORTABLE TRUTHS Testing is good Testable applications are better

Slide 9

Slide 9 text

SO WHAT CAN WE DO?

Slide 10

Slide 10 text

IT’S ABOUT TOOLS

Slide 11

Slide 11 text

IT’S ABOUT STRATEGIES

Slide 12

Slide 12 text

AUTOMATION IS KEY

Slide 13

Slide 13 text

AUTOMATION IS KEY “Write a script that will run all your tests before you go live”

Slide 14

Slide 14 text

AUTOMATION IS KEY “Tell your version control system to run your tests on commit or push”

Slide 15

Slide 15 text

AUTOMATION IS KEY http://jenkins-ci.org http://travis-ci.org

Slide 16

Slide 16 text

ARCHITECTURE “Simple systems can display complex behaviour but complex systems can only display simple behaviour”

Slide 17

Slide 17 text

ARCHITECTURE “Inside every great large application are many great small applications”

Slide 18

Slide 18 text

ARCHITECTURE “Your framework is a detail, not the core of your application.” -- Bob Martin

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

ARCHITECTURE “One of the great bugaboos of software applications over the years has been infiltration of business logic into the user interface code.” -- Alistair Cockburn http://alistair.cockburn.us/Hexagonal+architecture

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

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 23

Slide 23 text

DEPENDENCY INJECTION “Pass objects and their methods other objects that are required for the task.”

Slide 24

Slide 24 text

DEPENDENCY INJECTION 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 25

Slide 25 text

DEPENDENCY INJECTION _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 26

Slide 26 text

DEPENDENCY INJECTION _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 27

Slide 27 text

MOCK OBJECTS “Mock objects allow you to test code in proper isolation”

Slide 28

Slide 28 text

MOCK OBJECTS Database connections Web services File system operations

Slide 29

Slide 29 text

HOW DO WE TEST 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'); }

Slide 30

Slide 30 text

HOW DO WE TEST THIS? getMockBuilder('\Grumpy\Controller\Request') ! ! ! ->disableOriginalConstructor() ! ! ! ->getMock(); ! ! $mockController->expects($this->once)) ! ! ! ->method('getUri') ! ! ! ->will($this->returnValue($testUri)); ! ! $testUserLevel = 'admin'; ! ! $acl = new \Grumpy\Acl($mockRequest, $testUserLevel); ! ! $this->assertTrue( ! ! ! $acl->accessAllowed(), ! ! ! 'admin user should have access to purge accounts' ! ! ); ! } }

Slide 31

Slide 31 text

HOW DO WE TEST THIS? “Protected and private methods and attributes are difficult to test properly”

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

ATTRIBUTES? PHP’S Reflection API PHPUnit lets you check attribute values but not set them

Slide 39

Slide 39 text

HOW DO YOU TEST THIS? “If your unit test actually uses the database, you are doing it wrong”

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

HOW DO YOU TEST THIS? class BarTest extends PHPUnit_Framework_Testcase { ! public function testGetBazById() ! { ! ! ... ! ! $testBar = new Bar(); ! ! $testBar->setDb($mockDb); ! ! $testResults = $testBar->getBazById($bazId); ! ! $this->assertEquals( ! ! ! $expectedResults, ! ! ! $testResults, ! ! ! 'Did not get expected baz result set' ! ! ); ! } }

Slide 43

Slide 43 text

HOW DO YOU TEST THIS? “API calls should be done via wrapper methods”

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

HOW DO WE TEST THIS? class HipsterApiTest extends PHPUnit_Framework_Testcase { ! public function testGetBands() ! { ! ! ... ! ! $hipsterApiWrapper = new HipsterApiWrapper($mockHipsterApi); ! ! $testData = $hipsterApiWrapper->getBands(); ! ! ! ! ! $this->assertEquals( ! ! ! $expectedData, ! ! ! $testData, ! ! ! 'Did not get expected getBands() result from HipsterApi' ! ! ); ! } }

Slide 47

Slide 47 text

ENVIRONMENTS “Your app shouldn’t care what environment it runs in.”

Slide 48

Slide 48 text

ENVIRONMENTS “Keep config files for each environment under version control” https:/ /github.com/flogic/ whiskey_disk

Slide 49

Slide 49 text

RESOURCES PHPUnit http:/ /phpunit.de

Slide 50

Slide 50 text

RESOURCES Behat http:/ /behat.org

Slide 51

Slide 51 text

BEHAT Feature: Do a Google search In order to find pages about Behat As a user I want to be able to use google.com to locate search results @javascript Scenario: I search for Behat Given I fill in "Behat github" for "q" When I press "Google Search" Then I should see "Trying to use Mink"

Slide 52

Slide 52 text

RESOURCES The people sitting next to you

Slide 53

Slide 53 text

RESOURCES The Grumpy Programmer’s Guide to Building Testable PHP Applications http:/ /grumpy-testing.com

Slide 54

Slide 54 text

RESOURCES The Grumpy Programmer’s PHPUnit Cookbook http:/ /grumpy-phpunit.com

Slide 55

Slide 55 text

THANK YOU! @grmpyprogrammer http:/ /www.littlehart.net/atthekeyboard https:/ /speakerdeck.com/grumpycanuck/testing- online-crazy-glue