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

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.

Jeff Carouth

April 26, 2014
Tweet

More Decks by Jeff Carouth

Other Decks in Programming

Transcript

  1. “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
  2. “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
  3. 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.
  4. 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
  5. Unit testing helps us by allowing us to focus on

    established best practices of OOP/D. Takeaway #2
  6. class CartTest extends \PHPUnit_Framework_TestCase! {! public function testCartIsInitiallyEmpty()! {! $cart

    = new \Example\Cart();! $this->assertEquals(0, $cart->count());! }! }!
  7. ! 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
  8. ! 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)
  9. class CartTest extends \PHPUnit_Framework_TestCase! {! //...snip...! ! public function testCanAddOneProductToCart()!

    {! $cart = new \Example\Cart();! $cart->addProduct(new \Example\Product());! $this->assertEquals(1, $cart->count());! }! }!
  10. ! 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
  11. ! 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.
  12. class Cart! {! private $contents;! ! public function __construct()! {!

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

    $this->contents = array();! }! ! public function count()! {! return count($this->contents);! }! ! public function addProduct($product)! {! $this->contents[] = $product;! }! }!
  15. ! 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)
  16. 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());! }! }!
  17. 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());! }! }!
  18. ! 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)
  19. 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());! }! }!
  20. 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));! }! }!
  21. ! 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.
  22. ! 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)
  23. Use the tests cases you write to document your code’s

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

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

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

    0)! {! $this->price = $price;! }! ! public function getPrice()! {! return $this->price;! }! }!
  28. ! 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)
  29. 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());! }! }!
  30. 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());! }! }!
  31. 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());! }! }!
  32. 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());! }! }!
  33. ! 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)
  34. 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;! }! }!
  35. class CartTest extends \PHPUnit_Framework_TestCase! {! //...snip...! ! public function testCanAddOneProductToCart()!

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

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

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

    collaborators your object will use. Takeaway #4