From typing the test to testing the type
Upcoming SlideShare
Loading in...5
×

Like this? Share it with your network

Share

From typing the test to testing the type

  • 1,767 views
Uploaded on

PHP unit testing + new PHPUnit patch for type testing functionality ...

PHP unit testing + new PHPUnit patch for type testing functionality

Seems bullet points are not working and some of the slides are not so clear because of Slideshare conversion.

Presentation given at phpBenelux meeting August 25, 2010

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
    Be the first to like this
No Downloads

Views

Total Views
1,767
On Slideshare
1,762
From Embeds
5
Number of Embeds
2

Actions

Shares
Downloads
9
Comments
0
Likes
0

Embeds 5

http://www.linkedin.com 4
https://www.linkedin.com 1

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide
  • It's not the responsibility of the class to create the new classes, it's the responsibility of the callee to provide these objects. For example : an object using a cache should not instantiate the cache, nor do cache::getInstance, instead the cache object should be provided to the object.

Transcript

  • 1. From typing the test to testing the type Wim Godden Cu.be Solutions User meeting Aug 25, 2010
  • 2. About me
    • Wim Godden
    • 3. PHP developer since 1997 (PHP/FI 2.0)
    • 4. ZCE and ZFCE
    • 5. Developer of OpenX
    • 6. Owner of Cu.be Solutions
    • 7. Currently : PHP Architect @ NMBS
  • 8. About you
    • Developers ? Managers ?
    • 9. OOP experience ?
    • 10. Testing :
      • What is unit testing ?
      • 11. Have you written any ?
  • 12. Brief agenda
    • What's unit testing ?
    • 13. How does it work ?
    • 14. When and how to test
    • 15. A complete example (with some pitfalls)
    • 16. Some hints & tips along the way
    • 17. Testing the type
  • 18. This is not...
    • a full tutorial on unit testing
    • 19. a complete step-by-step guide
    • 20. a typical unit testing presentation (as you'll notice...)
    • 21. -> Plenty of online resources, tutorials, conference talks, etc. about the regular stuff
  • 22. What's unit testing ?
      <Wikipedia> Unit testing is a software design and development method where the programmer gains confidence that individual units of source code are fit for use. </Wikipedia>
  • 23. What's unit testing ?
      <Wikipedia> Unit testing is a software design and development method where the programmer gains confidence that individual units of source code are fit for use. </Wikipedia>
  • 24. What's a unit ? <?php class Fridge { protected $beerBottles ; public function getBeerSupply() { return $this ->beerBottles; } public function addBeer( $bottles ) { $this ->beerBottles += $bottles ; return true ; } public function drinkBeer( $bottles ) { $this ->beerBottles -= $bottles ; return true ; } }
  • 25. Why unit test ?
    • Write code
    • 26. Add code
    • 27. Add more code
    • 28. At some point you will break something
      • Misspelling
      • 29. Typos (= instead of == in an if-statement)
      • 30. Backwards logic (== instead of !=, < instead of >)
      • 31. etc.
    • So what do you do ?
      • -> You look for the bug !
  • 32. Why unit test ?
    • So what happens when you find that bug ?
    • You fix it of course...
    • Only to find that you just caused 6 other things to break
  • 33. Why unit test ?
    • You test the smallest piece of code
    • 34. -> if a test fails, you know exactly where to look
    • 35. Early detection of bugs
    • 36. Tests are :
    • Functional tests -> “something's wrong” -> but where ?
    • 39. Unit tests -> “this is what's wrong in this piece of code”
  • 40. A simple example <?php class Fridge { protected $beerBottles ; public function addBeer( $bottles ) { $this ->beerBottles += $bottles ; return true ; } public function drinkBeer( $bottles ) { if ( $bottles >= $this ->beerBottles) { $this ->beerBottles -= $bottles ; return true ; } else { // We ran out of beer ! // $wive->sendToStore($beer, 'a lot'); return false ; } } public function getBeerSupply() { return $this ->beerBottles; } } <?php class FridgeTest extends PHPUnit_Framework_TestCase { private $fridge ; public function testAddBeer() { $this ->fridge = new Fridge(); $this ->assertTrue( $this ->fridge->addBeer( 15 )); } public function testDrinkBeer() { $this ->fridge = new Fridge(); $this ->assertTrue( $this ->fridge->drinkBeer( 15 )); } public function testGetBeerSupply() { $this ->fridge = new Fridge(); $this ->assertType( 'integer' , $this ->fridge->getBeerSupply() ); } }
  • 41. How does it work ?
  • 42. Running the test
    • > phpunit <test-filename.php>
    • 43. Output :
    • 44. ...
    • 45. Time: 0 seconds
    • 46. OK (3 tests, 3 assertions)
  • 47. Assertions
  • 55. Test case environment
    • Each test must be run in an identical environment
    • 56. Tests should run independent of the result of a previous test
    • 57. How ?  Fixtures
      • setUp() : creates the initial state
      • 58. tearDown() : destroys the state
  • 59. Fixtures
    • Test1
      • Starts with empty fridge
      • 60. Fill the fridge with 15 beers
      • 61. Run the test (take 14 beers)
    • Test2
      • Starts with 1 beer in fridge
      • 62. Fill the fridge with 15 beers
      • 63. Run the test (see if there's 15 beers in the fridge)
      • 64. -> FAIL !
  • 65. Fixtures
    • setUp()
    • 66. Test1
      • Starts with empty fridge
      • 67. Fill the fridge with 15 beers
      • 68. Run the test (take 14 beers)
    • tearDown()
      • Drink all the beer left
      • 69. in the fridge
    • setUp()
    • 70. Test2
      • Starts with empty fridge
      • 71. Fill the fridge with 15 beers
      • 72. Run the test (see if there's 15 beers in the fridge)
      • 73. -> SUCCESS !
    • tearDown()
  • 74. Fixtures example class FridgeTest extends PHPUnit_Framework_TestCase { private $fridge ; protected function setUp() { $this ->fridge = new Fridge(); } protected function tearDown() { $this ->fridge->drinkBeer( $this ->fridge->getBeerSupply() ); } public function testAddBeer() { $this ->assertTrue( $this ->fridge->addBeer( 15 )); } public function testDrinkBeer() { $this ->assertTrue( $this ->fridge->drinkBeer( 15 )); } public function testGetBeerSupply() { $this ->assertType( 'integer' , $this ->fridge->getBeerSupply() ); } }
  • 83. The do's and dont's class User { protected $age ; public function getAge() { return $this ->age; } public function setAge( $newAge ) { if ( $newAge ) { if ( $newAge >= 0 && $newAge <= 150 ) { $this ->age = $newAge ; return true ; } else { return false ; } } else { return false ; } } } class UserTest extends PHPUnit_Framework_TestCase { private $User ; protected function setUp() { $this ->User = new User(); } public function testGetAge() { $this ->assertType( 'integer' , $this ->User->getAge()); } protected static function provideAges() { return array ( array (rand( 0 , 150 )), array (rand( 0 , 150 )), array (rand( 0 , 150 )) ); } /** * @dataProvider provideAges */ public function testSetAge( $age ) { $this ->assertTrue( $this ->User->setAge( $age )); }
  • 84. Problems with this test
    • What about ages < 0 and > 150 ?
    • 85. What about non-integers ?
    • 86. What about empty string ? Null ?
    protected static function provideInvalidAges() { return array ( array (- 1 ), array ( 151 ), array ( &quot;not an int&quot; ), array ( &quot;&quot; ), array (NULL) ); } /** * @dataProvider provideInvalidAges */ public function testSetAgeInvalid( $age ) { $this ->assertFalse( $this ->User->setAge( $age )); }
  • 87. Good test ? protected static function provideAges() { return array ( array (rand( 0 , 150 )), array (rand( 0 , 150 )), array (rand( 0 , 150 )) ); } /** * @dataProvider provideAges */ public function testSetAge( $age ) { $this ->assertTrue( $this ->User->setAge( $age )); } protected static function provideInvalidAges() { return array ( array (- 1 ), array ( 151 ), array ( &quot;not an int&quot; ), array ( &quot;&quot; ), array (NULL) ); } /** * @dataProvider provideInvalidAges */ public function testSetAgeInvalid( $age ) { $this ->assertFalse( $this ->User->setAge( $age )); }
  • 88. Erratic test results !
  • 89. Bug in the test or the code ? public function setAge( $newAge ) { if ( $newAge ) { if ( $newAge >= 0 && $newAge <= 150 ) { $this ->age = $newAge ; return true ; } else { return false ; } } else { return false ; } } protected static function provideAges() { return array ( array (rand( 0 , 150 )), array (rand( 0 , 150 )), array (rand( 0 , 150 )) ); } /** * @dataProvider provideAges */ public function testSetAge( $age ) { $this ->assertTrue( $this ->User->setAge( $age )); }
  • 90. Bug in the code ! public function setAge( $newAge ) { if (is_int( $newAge )) { if ( $newAge >= 0 && $newAge <= 150 ) { $this ->age = $newAge ; return true ; } else { return false ; } } else { return false ; } } protected static function provideAges() { return array ( array (rand( 0 , 150 )), array (rand( 0 , 150 )), array (rand( 0 , 150 )) ); } /** * @dataProvider provideAges */ public function testSetAge( $age ) { $this ->assertTrue( $this ->User->setAge( $age )); }
  • 91. The code was buggy... what about the test ?
  • 92. Good tests...
    • always test minimum and maximum values
    • 93. always test edge cases
    • 94. So... good test ? We're testing :
      • valid parameters
      • 95. invalid parameters
      • 96. edge cases
    • But : are we actually testing setAge() ?
    • 97. -> We're just checking if setAge() returns true or false !
    protected static function provideAges() { return array ( array ( 0 ), array (rand( 1 , 149 )), array (rand( 1 , 149 )), array (rand( 1 , 149 )), array ( 150 ) ); }
  • 98. Good tests... public function testSetAgeActuallySets() { $newAge = mt_rand( 0 , 150 ); $this ->User->setAge( $newAge ); $this ->assertEquals( $newAge , $this ->User->getAge() ); } Good tests don't just cover code... they test the functionality of the code !
  • 99. Testing code – not always easy
    • Some things are hard to test :
    • The secret to writing good tests, is to write testable code
  • 105. How NOT to test try { $billingAddress = new Address(&quot;Meir 12&quot;, 2000, &quot;Antwerpen&quot;, &quot;BE&quot;); $officeAddress = new Address(&quot;Groenplaats 50&quot;, 2000, &quot;Antwerpen&quot;, &quot;BE&quot;); $customer = new Customer(402, &quot;Jan&quot;, &quot;De Man&quot;, 32, $billingAddress, $officeAddress); $product = new Product(92, &quot;Speculaas&quot;, 5.95); $invoice = new Invoice($customer); $invoice.addItemQuantity($product, 5); $lineItems = $invoice.getLineitems(); if ($lineItems.size() == 1) { $actualLineItem = $lineItems.get(0); $this->assertEquals($invoice, $actualLineItem.getInvoice()); $this->assertEquals($product, $actualLineItem.getProduct()); $this->assertEquals($quantity, $actualLineItem, getQuantity()); $this->assertEquals(5.95, $actualLineItem.getUnitPrice()); } else { $this->assertTrue(false, 'Invoice should have exactly one line item'); } deleteObject($invoice); deleteObject($product); deleteObject($customer); deleteObject($billingAddress); deleteObject($officeAddress); } catch (Exception $e) { // Whatever }
  • 106. How NOT to test try { $billingAddress = new Address(&quot;Meir 12&quot;, 2000, &quot;Antwerpen&quot;, &quot;BE&quot;); $officeAddress = new Address(&quot;Groenplaats 50&quot;, 2000, &quot;Antwerpen&quot;, &quot;BE&quot;); $customer = new Customer(402, &quot;Jan&quot;, &quot;De Man&quot;, 32, $billingAddress, $officeAddress); $product = new Product(92, &quot;Speculaas&quot;, 5.95); $invoice = new Invoice($customer); $invoice.addItemQuantity($product, 5); $lineItems = $invoice.getLineitems(); $this->assertEquals(1, $lineItems.size()); $actualLineItem = $lineItems.get(0); $this->assertEquals($invoice, $actualLineItem.getInvoice()); $this->assertEquals($product, $actualLineItem.getProduct()); $this->assertEquals($quantity, $actualLineItem, getQuantity()); $this->assertEquals(5.95, $actualLineItem.getUnitPrice()); deleteObject($invoice); deleteObject($product); deleteObject($customer); deleteObject($billingAddress); deleteObject($officeAddress); } catch (Exception $e) { // Whatever }
  • 107. How NOT to test try { $billingAddress = new Address(&quot;Meir 12&quot;, 2000, &quot;Antwerpen&quot;, &quot;BE&quot;); $officeAddress = new Address(&quot;Groenplaats 50&quot;, 2000, &quot;Antwerpen&quot;, &quot;BE&quot;); $customer = new Customer(402, &quot;Jan&quot;, &quot;De Man&quot;, 32, $billingAddress, $officeAddress); $product = new Product(92, &quot;Speculaas&quot;, 5.95); $invoice = new Invoice($customer); $invoice.addItemQuantity($product, 5); $lineItems = $invoice.getLineitems(); $this->assertEquals(1, $lineItems.size()); $actualLineItem = $lineItems.get(0); $this->assertEquals($invoice, $actualLineItem.getInvoice()); $this->assertEquals($product, $actualLineItem.getProduct()); $this->assertEquals($quantity, $actualLineItem, getQuantity()); $this->assertEquals(5.95, $actualLineItem.getUnitPrice()); try { deleteObject($invoice); } catch (Exception $e) { // Whatever } try { deleteObject($product); } catch (Exception $e) { // Whatever } try { deleteObject($customer); } catch (Exception $e) { // Whatever } try { deleteObject($billingAddress); } catch (Exception $e) { // Whatever } try { deleteObject($officeAddress); } catch (Exception $e) { // Whatever } } catch (Exception $e) { // Whatever }
  • 108. Make a testObject array protected $testObjects; public function testInvoice() { $billingAddress = new Address(&quot;Meir 12&quot;, 2000, &quot;Antwerpen&quot;, &quot;BE&quot;); $this->addTestObject($billingAddress); $officeAddress = new Address(&quot;Groenplaats 50&quot;, 2000, &quot;Antwerpen&quot;, &quot;BE&quot;); $this->addTestObject($officeAddress); $customer = new Customer(402, &quot;Jan&quot;, &quot;De Man&quot;, 32, $billingAddress, $officeAddress); $this->addTestObject($customer); $product = new Product(92, &quot;Speculaas&quot;, 5.95); $this->addTestObject($product); $invoice = new Invoice($customer); $this->addTestObject($invoice); $invoice.addItemQuantity($product, 5); $lineItems = $invoice.getLineitems(); $this->assertEquals(1, $lineItems.size()); $actualLineItem = $lineItems.get(0); $this->assertEquals($invoice, $actualLineItem.getInvoice()); $this->assertEquals($product, $actualLineItem.getProduct()); $this->assertEquals($quantity, $actualLineItem, getQuantity()); $this->assertEquals(5.95, $actualLineItem.getUnitPrice()); } public function tearDown() { $this->deleteTestObjects(); }
      Note : this is a design decision from the very start of your project, since all objects need the same method for deletion
  • 109. Replace the unneeded data protected $testObjects; public function testInvoice() { $billingAddress = new TestAddress(); $this->addTestObject($billingAddress); $officeAddress = new TestAddress(); $this->addTestObject($officeAddress); $customer = new TestCustomer($billingAddress, $officeAddress); $this->addTestObject($customer); $product = new Product(92, &quot;Speculaas&quot;, 5.95); $this->addTestObject($product); $invoice = new Invoice($customer); $this->addTestObject($invoice); $invoice.addItemQuantity($product, 5); $lineItems = $invoice.getLineitems(); $this->assertEquals(1, $lineItems.size()); $actualLineItem = $lineItems.get(0); $this->assertEquals($invoice, $actualLineItem.getInvoice()); $this->assertEquals($product, $actualLineItem.getProduct()); $this->assertEquals($quantity, $actualLineItem, getQuantity()); $this->assertEquals(5.95, $actualLineItem.getUnitPrice()); } public function tearDown() { $this->deleteTestObjects(); }
  • 110. Instantiating objects (1/3)
    • Looks like a standard test-class relationship, with invoice being a 'leaf' class
    • 111. Until you look at the Invoice class
  • 112. Instantiating objects (2/3)
      Create a seam which separates your class from other classes
  • 113. Instantiating objects (3/3)
    • Don't do 'new Customer' in Invoice, instead do it in the test and pass it as a parameter
    • 114. Advantages :
      • Objects can be tested separately
      • 115. Hard-to-test classes can be mocked (replaced by a fake class)
    • Important : you have to keep this in mind when writing your code !
  • 116. Testable code
    • Good object orientation
    • 117. Dependency injection
    • 118. Uses TDD
  • 119. Test-Driven Development
    • Reverse programming logic :
    • If code is too complex -> hard to test
    • 122. Write the test first -> code will do what the test dictates
    • 123. It's a habit...
    • 124. and it's addicting !
  • 125. Random thoughts...
    • # lines test ≈ # lines code
      • Why ? Because you need to test invalid cases, edge cases, etc.
    • You can add your own assertions
    • 126. Multi-developer projects :
      • If you launch tests simultaneously -> unexpected (and unrepeatable) results -> agree on a token
      • 127. Agree on 'punishment' for who breaks the build (buy a beer, go get coffee, …)
        • -> makes people test locally before committing code
  • 128. We typed the test... Now let's test the type !
  • 129. What is it ?
    • A PHPUnit addon / patch
    • 130. Allows type validation during unit testing
    • 131. Time it took to build : a few days
    • 132. Time it takes for developers to use : none
      • Just add --check-param-types to your PHPUnit call
      • 133. See the magic happen
  • 134. How it works
    • It analyzes the docblock
    • 135. When the method is called from our test, it compares the type of each parameter with the type in the docblock :
    • 136. The result :
    /** * Cat constructor * @param string $name The name of the cat * @param int $lives The number of lives this cat has left */ PHPUnit 3.5 by Sebastian Bergmann. FF... Time: 1 second, Memory: 3.25Mb There were 2 failures: 1) CatTest::test__constructWithNegativeLives Invalid type calling Cat->__construct : parameter 1 ($name) should be of type string but got bool instead in /home/dev/paramtest/tests/CatTest.php 2) CatTest::test__constructWithNegativeLives Invalid type calling Cat->__construct : parameter 2 ($lives) should be of type int but got string instead in /home/dev/paramtest/tests/CatTest.php FAILURES! Tests: 5, Assertions: 8, Failures: 2. $cat = new Cat(true, &quot;9&quot;);
  • 137. Why it's useful
    • PHP's dynamic typing is great, but it can be a pain !
    • 138. If you write a library :
      • You want to be sure people pass the right parameter types
      • 139. You don't want to do is_int(), is_bool(), ... all the time
    • If you modify your function's parameters :
      • If you forget to modify the callee : warning will be shown
      • 140. If you forget to update your docblock : warning will be shown
    • Parameter type validation was designed to help your data be consistent and of the type expected during design !
    • 141. (and it forces you to keep the docblock updated,
    • 142. which is good for API documentation quality !)
  • 143. How to install and run
    • Go to http://github.com/wimg/phpunit
    • 144. Click on 'Download Source'
    • 145. Run it :
    • 146. > php <path-to-phpunit.php> --check-param-types <your-test.php>
    • 147. Be amazed at how many type issues you have ;-)
  • 148. Warning
    • Work in progress
    • 149. Slows down unit tests by factor of 2.5
    • 150. MySQL = trouble
      • (but we're looking into it)
  • 151. Let's refresh
    • Tests should always pass or fail, but never behave erratically
      • -> Consistency is the key to code quality
      • 152. -> Consistency gives the developer confidence in testing
    • Always test valid and invalid cases
    • 153. Always test the edge cases
    • 154. Don't rely on completion of other tests
      • -> Use fixtures
    • Use docblocks
    • 155. Test your types for consistency
    • 156. -> Unit tests take a developer from :
    • 157. This code should work
    • 158. to
    • 159. I can prove it works
  • 160. Cu.be Solutions
    • Founded in 2010
    • 161. Spinoff of FirstLink Networks (founded in 2000)
    • 162. High-quality open source solutions
    • 163. Centered around PHP
    • 164. Contribute to open source projects
    • 165. (ZF, Xdebug, PHPUnit, OpenX, ...)
    • 166. We're hiring !
    • 167. -> http://cu.be/jobs
    • 168. -> info@cu.be
  • 169.
      Questions ?
  • 170.
      Thanks !
  • 171. Contact
    • Web http://joind.in/talk/view/xxx
                • http://cu.be
                • 172. http://techblog.wimgodden.be
    • Slides http://www.slideshare.net/wimg
    • 173. Twitter @wimgtr
    • 174. E-mail [email_address]