SlideShare a Scribd company logo
From typing the test to testing the type Wim Godden Cu.be Solutions User meeting Aug 25, 2010
About me Wim Godden
PHP developer since 1997 (PHP/FI 2.0)
ZCE and ZFCE
Developer of OpenX
Owner of Cu.be Solutions
Currently : PHP Architect @ NMBS
About you Developers ? Managers ?
OOP experience ?
Testing : What is unit testing ?
Have you written any ?
Brief agenda What's unit testing ?
How does it work ?
When and how to test
A complete example (with some pitfalls)
Some hints & tips along the way
Testing the type
This is not... a full tutorial on unit testing
a complete step-by-step guide
a typical unit testing presentation (as you'll notice...)
-> Plenty of online resources, tutorials, conference talks, etc. about the regular stuff
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>
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>
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 ; } }
Why unit test ? Write code
Add code
Add more code
At some point you will break something Misspelling
Typos (= instead of == in an if-statement)
Backwards logic (== instead of !=, < instead of >)
etc. So what do you do ? ->  You look for the bug !
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
Why unit test ? You test the smallest piece of code
-> if a test fails, you know exactly where to look
Early detection of bugs
Tests are : automated
repeatable
consistent Functional tests -> “something's wrong” -> but where ?
Unit tests -> “this is what's wrong in this piece of code”
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() ); } }
How does it work ?
Running the test > phpunit <test-filename.php>
Output :
...
Time: 0 seconds
OK (3 tests, 3 assertions)
Assertions AssertEquals
AssertTrue
AssertFalse
AssertType
AssertGreaterThan

More Related Content

From typing the test to testing the type

  • 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)
  • 6. Owner of Cu.be Solutions
  • 7. Currently : PHP Architect @ NMBS
  • 8. About you Developers ? Managers ?
  • 10. Testing : What is unit testing ?
  • 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
  • 18. This is not... a full tutorial on unit testing
  • 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
  • 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
  • 36. Tests are : automated
  • 38. consistent 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>
  • 44. ...
  • 46. OK (3 tests, 3 assertions)
  • 53.
  • 54. You can also add your own !
  • 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
  • 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)
  • 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 Running the tests results in : setUp()
  • 82. tearDown() 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 ?
  • 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 )); }
  • 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
  • 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 : Private methods
  • 104. Too many conditional statements 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
  • 119. Test-Driven Development Reverse programming logic : Write the test
  • 120. then
  • 121. Write the code If code is too complex -> hard to test
  • 122. Write the test first -> code will do what the test dictates
  • 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'
  • 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 :
  • 158. to
  • 159. I can prove it works
  • 161. Spinoff of FirstLink Networks (founded in 2000)
  • 164. Contribute to open source projects
  • 165. (ZF, Xdebug, PHPUnit, OpenX, ...)
  • 171. Contact Web http://joind.in/talk/view/xxx http://cu.be
  • 172. http://techblog.wimgodden.be Slides http://www.slideshare.net/wimg

Editor's Notes

  1. It&apos;s not the responsibility of the class to create the new classes, it&apos;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.