Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

From typing the test to testing the type

1,807 views

Published on

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

Published in: Technology
  • Be the first to comment

  • Be the first to like this

From typing the test to testing the type

  1. 1. From typing the test to testing the type Wim Godden Cu.be Solutions User meeting Aug 25, 2010
  2. 2. About me <ul><li>Wim Godden
  3. 3. PHP developer since 1997 (PHP/FI 2.0)
  4. 4. ZCE and ZFCE
  5. 5. Developer of OpenX
  6. 6. Owner of Cu.be Solutions
  7. 7. Currently : PHP Architect @ NMBS </li></ul>
  8. 8. About you <ul><li>Developers ? Managers ?
  9. 9. OOP experience ?
  10. 10. Testing : </li><ul><li>What is unit testing ?
  11. 11. Have you written any ? </li></ul></ul>
  12. 12. Brief agenda <ul><li>What's unit testing ?
  13. 13. How does it work ?
  14. 14. When and how to test
  15. 15. A complete example (with some pitfalls)
  16. 16. Some hints & tips along the way
  17. 17. Testing the type </li></ul>
  18. 18. This is not... <ul><li>a full tutorial on unit testing
  19. 19. a complete step-by-step guide
  20. 20. a typical unit testing presentation (as you'll notice...)
  21. 21. -> Plenty of online resources, tutorials, conference talks, etc. about the regular stuff </li></ul>
  22. 22. What's unit testing ? <ul><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> </ul>
  23. 23. What's unit testing ? <ul><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> </ul>
  24. 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. 25. Why unit test ? <ul><li>Write code
  26. 26. Add code
  27. 27. Add more code
  28. 28. At some point you will break something </li><ul><li>Misspelling
  29. 29. Typos (= instead of == in an if-statement)
  30. 30. Backwards logic (== instead of !=, < instead of >)
  31. 31. etc. </li></ul><li>So what do you do ? </li><ul><li>-> You look for the bug ! </li></ul></ul>
  32. 32. Why unit test ? <ul><li>So what happens when you find that bug ? </li></ul><ul><li>You fix it of course... </li></ul><ul><li>Only to find that you just caused 6 other things to break </li></ul>
  33. 33. Why unit test ? <ul><li>You test the smallest piece of code
  34. 34. -> if a test fails, you know exactly where to look
  35. 35. Early detection of bugs
  36. 36. Tests are : </li><ul><li>automated
  37. 37. repeatable
  38. 38. consistent </li></ul><li>Functional tests -> “something's wrong” -> but where ?
  39. 39. Unit tests -> “this is what's wrong in this piece of code” </li></ul>
  40. 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. 41. How does it work ?
  42. 42. Running the test <ul><li>> phpunit <test-filename.php>
  43. 43. Output :
  44. 44. ...
  45. 45. Time: 0 seconds
  46. 46. OK (3 tests, 3 assertions) </li></ul>
  47. 47. Assertions <ul><li>AssertEquals
  48. 48. AssertTrue
  49. 49. AssertFalse
  50. 50. AssertType
  51. 51. AssertGreaterThan
  52. 52. AssertNotNull
  53. 53.
  54. 54. You can also add your own ! </li></ul>
  55. 55. Test case environment <ul><li>Each test must be run in an identical environment
  56. 56. Tests should run independent of the result of a previous test
  57. 57. How ?  Fixtures </li><ul><li>setUp() : creates the initial state
  58. 58. tearDown() : destroys the state </li></ul></ul>
  59. 59. Fixtures <ul><li>Test1 </li><ul><li>Starts with empty fridge
  60. 60. Fill the fridge with 15 beers
  61. 61. Run the test (take 14 beers) </li></ul></ul><ul><li>Test2 </li><ul><li>Starts with 1 beer in fridge
  62. 62. Fill the fridge with 15 beers
  63. 63. Run the test (see if there's 15 beers in the fridge)
  64. 64. -> FAIL ! </li></ul></ul>
  65. 65. Fixtures <ul><li>setUp()
  66. 66. Test1 </li><ul><li>Starts with empty fridge
  67. 67. Fill the fridge with 15 beers
  68. 68. Run the test (take 14 beers) </li></ul><li>tearDown() </li><ul><li>Drink all the beer left
  69. 69. in the fridge </li></ul></ul><ul><li>setUp()
  70. 70. Test2 </li><ul><li>Starts with empty fridge
  71. 71. Fill the fridge with 15 beers
  72. 72. Run the test (see if there's 15 beers in the fridge)
  73. 73. -> SUCCESS ! </li></ul><li>tearDown() </li></ul>
  74. 74. Fixtures example <ul>Running the tests results in : <li>setUp()
  75. 75. testAddBeer()
  76. 76. tearDown()
  77. 77. setUp()
  78. 78. teatDrinkBeer()
  79. 79. tearDown()
  80. 80. setUp()
  81. 81. testGetBeerSupply()
  82. 82. tearDown() </li></ul>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. 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. 84. Problems with this test <ul><li>What about ages < 0 and > 150 ?
  85. 85. What about non-integers ?
  86. 86. What about empty string ? Null ? </li></ul>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. 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. 88. Erratic test results !
  89. 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. 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. 91. The code was buggy... what about the test ?
  92. 92. Good tests... <ul><li>always test minimum and maximum values
  93. 93. always test edge cases
  94. 94. So... good test ? We're testing : </li><ul><li>valid parameters
  95. 95. invalid parameters
  96. 96. edge cases </li></ul><li>But : are we actually testing setAge() ?
  97. 97. -> We're just checking if setAge() returns true or false ! </li></ul>protected static function provideAges() { return array ( array ( 0 ), array (rand( 1 , 149 )), array (rand( 1 , 149 )), array (rand( 1 , 149 )), array ( 150 ) ); }
  98. 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. 99. Testing code – not always easy <ul><li>Some things are hard to test : </li><ul><li>Private methods
  100. 100. Final methods
  101. 101. Long methods
  102. 102. Singletons / static methods
  103. 103. Deep inheritance
  104. 104. Too many conditional statements </li></ul><li>The secret to writing good tests, is to write testable code </li></ul>
  105. 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. 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. 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. 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(); } <ul>Note : this is a design decision from the very start of your project, since all objects need the same method for deletion </ul>
  109. 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. 110. Instantiating objects (1/3) <ul><li>Looks like a standard test-class relationship, with invoice being a 'leaf' class
  111. 111. Until you look at the Invoice class </li></ul>
  112. 112. Instantiating objects (2/3) <ul>Create a seam which separates your class from other classes </ul>
  113. 113. Instantiating objects (3/3) <ul><li>Don't do 'new Customer' in Invoice, instead do it in the test and pass it as a parameter
  114. 114. Advantages : </li><ul><li>Objects can be tested separately
  115. 115. Hard-to-test classes can be mocked (replaced by a fake class) </li></ul><li>Important : you have to keep this in mind when writing your code ! </li></ul>
  116. 116. Testable code <ul><li>Good object orientation
  117. 117. Dependency injection
  118. 118. Uses TDD </li></ul>
  119. 119. Test-Driven Development <ul><li>Reverse programming logic : </li><ul><li>Write the test
  120. 120. then
  121. 121. Write the code </li></ul><li>If code is too complex -> hard to test
  122. 122. Write the test first -> code will do what the test dictates
  123. 123. It's a habit...
  124. 124. and it's addicting ! </li></ul>
  125. 125. Random thoughts... <ul><li># lines test ≈ # lines code </li><ul><li>Why ? Because you need to test invalid cases, edge cases, etc. </li></ul><li>You can add your own assertions
  126. 126. Multi-developer projects : </li><ul><li>If you launch tests simultaneously -> unexpected (and unrepeatable) results -> agree on a token
  127. 127. Agree on 'punishment' for who breaks the build (buy a beer, go get coffee, …) </li><ul><li>-> makes people test locally before committing code </li></ul></ul></ul>
  128. 128. We typed the test... Now let's test the type !
  129. 129. What is it ? <ul><li>A PHPUnit addon / patch
  130. 130. Allows type validation during unit testing
  131. 131. Time it took to build : a few days
  132. 132. Time it takes for developers to use : none </li><ul><li>Just add --check-param-types to your PHPUnit call
  133. 133. See the magic happen </li></ul></ul>
  134. 134. How it works <ul><li>It analyzes the docblock
  135. 135. When the method is called from our test, it compares the type of each parameter with the type in the docblock :
  136. 136. The result : </li></ul>/** * 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. 137. Why it's useful <ul><li>PHP's dynamic typing is great, but it can be a pain !
  138. 138. If you write a library : </li><ul><li>You want to be sure people pass the right parameter types
  139. 139. You don't want to do is_int(), is_bool(), ... all the time </li></ul><li>If you modify your function's parameters : </li><ul><li>If you forget to modify the callee : warning will be shown
  140. 140. If you forget to update your docblock : warning will be shown </li></ul><li>Parameter type validation was designed to help your data be consistent and of the type expected during design !
  141. 141. (and it forces you to keep the docblock updated,
  142. 142. which is good for API documentation quality !) </li></ul>
  143. 143. How to install and run <ul><li>Go to http://github.com/wimg/phpunit
  144. 144. Click on 'Download Source'
  145. 145. Run it :
  146. 146. > php <path-to-phpunit.php> --check-param-types <your-test.php>
  147. 147. Be amazed at how many type issues you have ;-) </li></ul>
  148. 148. Warning <ul><li>Work in progress
  149. 149. Slows down unit tests by factor of 2.5
  150. 150. MySQL = trouble </li><ul><li>(but we're looking into it) </li></ul></ul>
  151. 151. Let's refresh <ul><li>Tests should always pass or fail, but never behave erratically </li><ul><li>-> Consistency is the key to code quality
  152. 152. -> Consistency gives the developer confidence in testing </li></ul><li>Always test valid and invalid cases
  153. 153. Always test the edge cases
  154. 154. Don't rely on completion of other tests </li><ul><li>-> Use fixtures </li></ul><li>Use docblocks
  155. 155. Test your types for consistency
  156. 156. -> Unit tests take a developer from :
  157. 157. This code should work
  158. 158. to
  159. 159. I can prove it works </li></ul>
  160. 160. Cu.be Solutions <ul><li>Founded in 2010
  161. 161. Spinoff of FirstLink Networks (founded in 2000)
  162. 162. High-quality open source solutions
  163. 163. Centered around PHP
  164. 164. Contribute to open source projects
  165. 165. (ZF, Xdebug, PHPUnit, OpenX, ...)
  166. 166. We're hiring !
  167. 167. -> http://cu.be/jobs
  168. 168. -> info@cu.be </li></ul>
  169. 169. <ul>Questions ? </ul>
  170. 170. <ul>Thanks ! </ul>
  171. 171. Contact <ul><li>Web http://joind.in/talk/view/xxx </li><ul><ul><ul><ul><ul><ul><li>http://cu.be
  172. 172. http://techblog.wimgodden.be </li></ul></ul></ul></ul></ul></ul><li>Slides http://www.slideshare.net/wimg
  173. 173. Twitter @wimgtr
  174. 174. E-mail [email_address] </li></ul>

×