Guiding Object-Oriented Design with Tests

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 follow the path of designing objects within a realistic problem domain.

0f930e13633535c1c4041e95b8881308?s=128

Jeff Carouth

April 26, 2014
Tweet

Transcript

  1. PRESENTED BY JEFF CAROUTH @jcarouth Guiding Object-Oriented Design with Tests

  2. PRESENTED BY JEFF CAROUTH @jcarouth Guiding Object-Oriented Design with Tests

  3. “The current fanatical TDD experience leads to a primary focus

    on the unit tests, because those are the tests capable of driving the code design (the original justification for test-first).” – DHH http://david.heinemeierhansson.com/2014/tdd-is-dead-long-live-testing.html
  4. “Is it really surprising that someone who chooses to stuff

    an entire application’s logic into active record models doesn’t like unit tests?” – me https://twitter.com/jcarouth/status/458963211484557312
  5. Why Test? 1.

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

  7. Traditional Development

  8. 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.
  9. 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
  10. OOP/OOD 2.

  11. ! Communication over implementation

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

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

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

  15. The Problem

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

    = new \Example\Cart();! $this->assertEquals(0, $cart->count());! }! }!
  17. ! 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
  18. namespace Example;! class Cart! {! public function count()! {! return

    0;! }! }!
  19. ! 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)
  20. Business Requirement A customer should be able to add products

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

    {! $cart = new \Example\Cart();! $cart->addProduct(new \Example\Product());! $this->assertEquals(1, $cart->count());! }! }!
  22. ! 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
  23. class Cart! {! //...snip...! public function addProduct($product)! {! ! }!

    }! namespace Example;! class Product! {! ! }!
  24. ! 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.
  25. class Cart! {! private $contents;! ! public function __construct()! {!

    $this->contents = array();! }! ! //...snip...! ! public function addProduct($product)! {! $this->contents[] = $product;! }! }!
  26. ! 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.
  27. class Cart! {! private $contents;! ! public function __construct()! {!

    $this->contents = array();! }! ! public function count()! {! return count($this->contents);! }! ! public function addProduct($product)! {! $this->contents[] = $product;! }! }!
  28. ! 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)
  29. 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());! }! }!
  30. 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());! }! }!
  31. ! 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)
  32. 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());! }! }!
  33. 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));! }! }!
  34. ! 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.
  35. class Cart implements \Countable! {! //...snip...! }!

  36. ! 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)
  37. Use the tests cases you write to document your code’s

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

    to the sum of the products’ prices in a customer’s cart.
  39. 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());! }! }!
  40. class Cart implements \Countable! {! //...snip...! ! public function subtotal()!

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

    0)! {! $this->price = $price;! }! ! public function getPrice()! {! return $this->price;! }! }!
  42. ! 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)
  43. But...Unit Tests

  44. 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());! }! }!
  45. 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());! }! }!
  46. But...interfaces

  47. But...interfaces roles

  48. 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());! }! }!
  49. 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());! }! }!
  50. namespace Example;! interface Sellable! {! public function getPrice();! }!

  51. ! 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)
  52. 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;! }! }!
  53. class CartTest extends \PHPUnit_Framework_TestCase! {! //...snip...! ! public function testCanAddOneProductToCart()!

    {! $this->cart->addProduct($this->product(500, false));! $this->assertEquals(1, count($this->cart));! }! ! //...snip...! }!
  54. ! 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)
  55. 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());! }! }
  56. 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;! }! }!
  57. Tip Type Hint on Roles Rather than Implementations public function

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

    cart but can only apply to certain types of products.
  59. 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());! }! }
  60. 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;! }! }
  61. ! 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)
  62. You can mock nonexistent interfaces to help you design the

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

  64. Tests are sandboxes for your objects

  65. Focus on Communication

  66. Listen to your tests and refactor

  67. Further Reading

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

  69. Thank You @jcarouth joind.in/10825