Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Guiding Object-Oriented Design with Tests

0f930e13633535c1c4041e95b8881308?s=47 Jeff Carouth
December 20, 2013

Guiding Object-Oriented Design with Tests

A test suite is a very useful tool in any application. Often times people refer to unit test suites as a safety net for refactoring and modification. While that is one benefit of having a comprehensive testing strategy and implementation, the real power of unit testing comes when you learn to allow the tests you are writing to inform the design of your application. In this session we will explore the why and how of writing tests for code architecture and object-oriented design purposes. We will look at situations where tests are telling us we are introducing smells into our codebase and how we can correct those smells.

0f930e13633535c1c4041e95b8881308?s=128

Jeff Carouth

December 20, 2013
Tweet

Transcript

  1. Guiding Object-Oriented Design with Tests Jeff Carouth // @jcarouth

  2. Why Test? 1.

  3. Software Development Learning Feedback the process of requires which requires

  4. Traditional Development

  5. Unit Testing in the context of object-oriented design helps us

    clarify what our objects are supposed to do and how they collaborate to accomplish that goal.
  6. Testing, specifically unit testing, aids our goals of using object-oriented

    programming techniques by giving us the feedback loop we need to inform our design. Takeaway #1
  7. OOP/OOD 2.

  8. Communication over implementation

  9. Value code that is easier to maintain over code that

    is easier to write.
  10. Unit testing helps us by allowing us to focus on

    established best practices of OOP/D. Takeaway #2
  11. TDD & OOP 3.

  12. The Problem

  13. class CartTest extends \PHPUnit_Framework_TestCase { public function testCartIsInitiallyEmpty() { $cart

    = new \Example\Cart(); $this->assertEquals(0, $cart->count()); } }
  14. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist F Fatal error: Class 'Example\Cart' not found
  15. namespace Example; class Cart { public function count() { return

    0; } }
  16. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist . Time: 422 ms, Memory: 4.00Mb OK (1 test, 1 assertion)
  17. Business Requirement A customer should be able to add products

    to his or her cart.
  18. class CartTest extends \PHPUnit_Framework_TestCase { //...snip... public function testCanAddOneProductToCart() {

    $cart = new \Example\Cart(); $cart->addProduct(new \Example\Product()); $this->assertEquals(1, $cart->count()); } }
  19. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist F Fatal error: Call to undefined method Example\Cart::addProduct() Fatal error: Class 'Example\Product' not found
  20. class Cart { //...snip... public function addProduct($product) { } }

    namespace Example; class Product { }
  21. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist .F Time: 56 ms, Memory: 4.00Mb There was 1 failure: 1) CartTest::testCanAddOneProductToCart Failed asserting that 0 matches expected 1. /projects/testing-ood/tests/Example/CartTest.php:16 FAILURES! Tests: 2, Assertions: 2, Failures: 1.
  22. class Cart { private $contents; public function __construct() { $this->contents

    = array(); } //...snip... public function addProduct($product) { $this->contents[] = $product; } }
  23. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist .F Time: 48 ms, Memory: 4.00Mb There was 1 failure: 1) CartTest::testCanAddOneProductToCart Failed asserting that 0 matches expected 1. /projects/testing-ood/tests/Example/CartTest.php:16 FAILURES! Tests: 2, Assertions: 2, Failures: 1.
  24. class Cart { private $contents; public function __construct() { $this->contents

    = array(); } public function count() { return count($this->contents); } public function addProduct($product) { $this->contents[] = $product; } }
  25. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist .. Time: 48 ms, Memory: 4.00Mb OK (2 tests, 2 assertions)
  26. class CartTest extends \PHPUnit_Framework_TestCase { public function testCartIsInitiallyEmpty() { $cart

    = new \Example\Cart(); $this->assertEquals(0, $cart->count()); } public function testCanAddOneProductToCart() { $cart = new \Example\Cart(); $cart->addProduct(new \Example\Product()); $this->assertEquals(1, $cart->count()); } }
  27. class CartTest extends \PHPUnit_Framework_TestCase { public function setUp() { $this->cart

    = new \Example\Cart(); } public function tearDown() { $this->cart = null; } public function testCartIsInitiallyEmpty() { $this->assertEquals(0, $this->cart->count()); } public function testCanAddOneProductToCart() { $this->cart->addProduct(new \Example\Product()); $this->assertEquals(1, $this->cart->count()); } }
  28. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist .. Time: 47 ms, Memory: 4.00Mb OK (2 tests, 2 assertions)
  29. class CartTest extends \PHPUnit_Framework_TestCase { public function setUp() { $this->cart

    = new \Example\Cart(); } public function tearDown() { $this->cart = null; } public function testCartIsInitiallyEmpty() { $this->assertEquals(0, $this->cart->count()); } public function testCanAddOneProductToCart() { $this->cart->addProduct(new \Example\Product()); $this->assertEquals(1, $this->cart->count()); } }
  30. class CartTest extends \PHPUnit_Framework_TestCase { //...snip... public function testCartImplementsCountable() {

    $this->assertInstanceOf('Countable', $this->cart); } public function testCartIsInitiallyEmpty() { $this->assertEquals(0, count($this->cart)); } public function testCanAddOneProductToCart() { $this->cart->addProduct(new \Example\Product()); $this->assertEquals(1, count($this->cart)); } }
  31. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist FF. Time: 94 ms, Memory: 3.75Mb There were 2 failures: 1) CartTest::testCartImplementsCountable Failed asserting that Example\Cart Object (...) is an instance of class "Countable". /projects/testing-ood/tests/Example/CartTest.php:18 2) CartTest::testCartIsInitiallyEmpty Failed asserting that 1 matches expected 0. /projects/testing-ood/tests/Example/CartTest.php:23 FAILURES! Tests: 3, Assertions: 3, Failures: 2.
  32. class Cart implements \Countable { //...snip... }

  33. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist ... Time: 47 ms, Memory: 3.75Mb OK (3 tests, 3 assertions)
  34. Use the tests cases you write to document your code’s

    behavior and roles for other developers–including your future self. Takeaway #3
  35. Business Requirement The subtotal for any given purchase is equivalent

    to the sum of the products’ prices in a customer’s cart.
  36. class CartTest extends \PHPUnit_Framework_TestCase { //...snip... public function testSubtotalSumsAllProductsInCart() {

    $this->cart->addProduct(new \Example\Product(2000)); $this->cart->addProduct(new \Example\Product(1500)); $this->assertEquals(3500, $this->cart->subtotal()); } }
  37. class Cart implements \Countable { //...snip... public function subtotal() {

    $runningTotal = 0; foreach ($this->contents as $product) { $runningTotal += $product->getPrice(); } return $runningTotal; } }
  38. class Product { private $price; public function __construct($price = 0)

    { $this->price = $price; } public function getPrice() { return $this->price; } }
  39. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist .... Time: 51 ms, Memory: 3.75Mb OK (4 tests, 4 assertions)
  40. But...Unit Tests

  41. class CartTest extends \PHPUnit_Framework_TestCase { //...snip... public function testSubtotalSumsAllProductsInCart() {

    $productOne = $this->getMockBuilder('\\Example\\Product') ->setMethods(array('getPrice')) ->getMock(); $productOne->expects($this->once()) ->method('getPrice') ->will($this->returnValue(2000)); $productTwo = $this->getMockBuilder('\\Example\\Product') ->setMethods(array('getPrice')) ->getMock(); $productTwo->expects($this->once()) ->method('getPrice') ->will($this->returnValue(1500)); $this->cart->addProduct($productOne); $this->cart->addProduct($productTwo); $this->assertEquals(3500, $this->cart->subtotal()); } }
  42. Tip You can mock interfaces that do not exist class

    InterfaceDoesNotExistTest extends \PHPUnit_Framework_TestCase { public function testItDoesNotExistLOL() { $mock = $this->getMockBuilder('\\My\\NonExistentInterface') ->setMethods(['lolForReal']) ->getMock(); $mock->expects($this->once()) ->method('lolForReal') ->will($this->returnValue('LOL')); $this->assertEquals('LOL', $mock->lolForReal()); } }
  43. But...interfaces

  44. But...interfaces roles

  45. class CartTest extends \PHPUnit_Framework_TestCase { //...snip... public function testSubtotalSumsAllProductsInCart() {

    $productOne = $this->getMockBuilder('\\Example\\Product') ->setMethods(array('getPrice')) ->getMock(); $productOne->expects($this->once()) ->method('getPrice') ->will($this->returnValue(2000)); $productTwo = $this->getMockBuilder('\\Example\\Product') ->setMethods(array('getPrice')) ->getMock(); $productTwo->expects($this->once()) ->method('getPrice') ->will($this->returnValue(1500)); $this->cart->addProduct($productOne); $this->cart->addProduct($productTwo); $this->assertEquals(3500, $this->cart->subtotal()); } }
  46. class CartTest extends \PHPUnit_Framework_TestCase { //...snip... public function testSubtotalSumsAllProductsInCart() {

    $productOne = $this->getMockBuilder('\\Example\\Sellable') ->getMock(); $productOne->expects($this->once()) ->method('getPrice') ->will($this->returnValue(2000)); $productTwo = $this->getMockBuilder('\\Example\\Sellable') ->getMock(); $productTwo->expects($this->once()) ->method('getPrice') ->will($this->returnValue(1500)); $this->cart->addProduct($productOne); $this->cart->addProduct($productTwo); $this->assertEquals(3500, $this->cart->subtotal()); } }
  47. namespace Example; interface Sellable { public function getPrice(); }

  48. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist .... Time: 68 ms, Memory: 3.75Mb OK (4 tests, 6 assertions)
  49. class CartTest extends \PHPUnit_Framework_TestCase { //...snip... public function testSubtotalSumsAllProductsInCart() {

    $this->cart->addProduct($this->product(2000)); $this->cart->addProduct($this->product(1500)); $this->assertEquals(3500, $this->cart->subtotal()); } private function product($price = 0, $expectPrice = true) { $product = $this->getMockBuilder('\\Example\\Sellable') ->getMock(); if ($expectPrice) { $product->expects($this->once()) ->method('getPrice') ->will($this->returnValue($price)); } return $product; } }
  50. class CartTest extends \PHPUnit_Framework_TestCase { //...snip... public function testCanAddOneProductToCart() {

    $this->cart->addProduct($this->product(500, false)); $this->assertEquals(1, count($this->cart)); } //...snip... }
  51. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist .... Time: 66 ms, Memory: 3.75Mb OK (4 tests, 6 assertions)
  52. class CartTest extends \PHPUnit_Framework_TestCase { public function testCartImplementsCountable() { $this->assertInstanceOf('Countable',

    $this->cart); } public function testCartIsInitiallyEmpty() { $this->assertEquals(0, count($this->cart)); } public function testCanAddOneProductToCart() { $this->cart->addProduct($this->product(500, false)); $this->assertEquals(1, count($this->cart)); } public function testSubtotalSumsAllProductsInCart() { $this->cart->addProduct($this->product(2000)); $this->cart->addProduct($this->product(1500)); $this->assertEquals(3500, $this->cart->subtotal()); } }
  53. namespace Example; class Cart implements \Countable { public function count()

    { return count($this->contents); } public function addProduct(Sellable $product) { $this->contents[] = $product; } public function subtotal() { $runningTotal = 0; foreach ($this->contents as $product) { $runningTotal += $product->getPrice(); } return $runningTotal; } }
  54. Tip Type Hint on Roles Rather than Implementations public function

    addProduct(Sellable $product) { $this->contents[] = $product; }
  55. Business Requirement Promotions are allowed to be applied to a

    cart but can only apply to certain types of products.
  56. class CartTest extends \PHPUnit_Framework_TestCase { public function testPromotionSubtractsAmountFromTotal() { $this->cart->addProduct($this->product(2000));

    $this->cart->addProduct($this->book(1000)); $bookPromotion = $this->getMockBuilder('\\Example\\Promotion') ->setMethods(array('applyTo')) ->getMock(); $bookPromotion->expects($this->any()) ->method('applyTo') ->will($this->returnCallback(function($product) { if ($product->getType() == 'Book') { return $product->getPrice() * 0.9; } return $product->getPrice(); })); $this->cart->setPromotion($bookPromotion); $this->assertEquals(2900, $this->cart->total()); } }
  57. class Cart implements \Countable { //...snip... public function setPromotion(Promotion $promo)

    { $this->promotion = $promo; } public function total() { $runningTotal = 0; foreach ($this->contents as $product) { if ($this->promotion !== null) { $runningTotal += $this->promotion->applyTo($product); } else { $runningTotal += $product->getPrice(); } } return $runningTotal; } }
  58. ! phpunit tests/Example/CartTest.php PHPUnit 3.7.28 by Sebastian Bergmann. Configuration read

    from /projects/testing-ood/phpunit.xml.dist ..... Time: 92 ms, Memory: 3.75Mb OK (5 tests, 11 assertions)
  59. You can mock nonexistent interfaces to help you design the

    collaborators your object will use. Takeaway #4
  60. Recap 4.

  61. Tests are sandboxes for your objects

  62. Focus on Communication

  63. Listen to your tests and refactor

  64. Further Reading

  65. http://crth.net/testing-ood Example code

  66. JEFF CAROUTH DEVELOPER AT LIFTOPIA @jcarouth jcarouth@gmail.com Freenode: #phpmentoring