From typing the test to testing the type Wim Godden Cu.be Solutions User meeting Aug 25, 2010
About me <ul><li>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 </li></ul>
About you <ul><li>Developers ? Managers ?
OOP experience ?
Testing : </li><ul><li>What is unit testing ?
Have you written any ? </li></ul></ul>
Brief agenda <ul><li>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 </li></ul>
This is not... <ul><li>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 </li></ul>
What's unit testing ? <ul><Wikipedia> Unit testing is a software design and development method where the programmer gains ...
What's unit testing ? <ul><Wikipedia> Unit testing is a software design and development method where the programmer gains ...
What's a unit ? <?php class  Fridge { protected  $beerBottles ; public function  getBeerSupply() { return  $this ->beerBot...
Why unit test ? <ul><li>Write code
Add code
Add more code
At some point you will break something </li><ul><li>Misspelling
Typos (= instead of == in an if-statement)
Backwards logic (== instead of !=, < instead of >)
etc. </li></ul><li>So what do you do ? </li><ul><li>->  You look for the bug ! </li></ul></ul>
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><...
Why unit test ? <ul><li>You test the smallest piece of code
-> if a test fails, you know exactly where to look
Early detection of bugs
Tests are : </li><ul><li>automated
repeatable
consistent </li></ul><li>Functional tests -> “something's wrong” -> but where ?
Unit tests -> “this is what's wrong in this piece of code” </li></ul>
A simple example <?php class  Fridge { protected  $beerBottles ; public function  addBeer( $bottles ) { $this ->beerBottle...
How does it work ?
Running the test <ul><li>> phpunit <test-filename.php>
Output :
...
Time: 0 seconds
OK (3 tests, 3 assertions) </li></ul>
Assertions <ul><li>AssertEquals
AssertTrue
AssertFalse
AssertType
AssertGreaterThan
Upcoming SlideShare
Loading in...5
×

From typing the test to testing the type

1,340

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
0 Comments
0 Likes
Statistics
Notes
  • Be the first to comment

  • Be the first to like this

No Downloads
Views
Total Views
1,340
On Slideshare
0
From Embeds
0
Number of Embeds
1
Actions
Shares
0
Downloads
12
Comments
0
Likes
0
Embeds 0
No embeds

No notes for slide
  • 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.
  • 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>
    1. A particular slide catching your eye?

      Clipping is a handy way to collect important slides you want to go back to later.

    ×