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. PRESENTED BY
    JEFF CAROUTH
    @jcarouth
    Guiding Object-Oriented
    Design with Tests

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  5. Why Test?
    1.

    View Slide

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

    View Slide

  7. Traditional
    Development

    View Slide

  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.

    View Slide

  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

    View Slide

  10. OOP/OOD
    2.

    View Slide

  11. !
    Communication
    over
    implementation

    View Slide

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

    View Slide

  13. Unit testing helps us by allowing us
    to focus on established best
    practices of OOP/D.
    Takeaway #2

    View Slide

  14. TDD & OOP
    3.

    View Slide

  15. The Problem

    View Slide

  16. class CartTest extends \PHPUnit_Framework_TestCase!
    {!
    public function testCartIsInitiallyEmpty()!
    {!
    $cart = new \Example\Cart();!
    $this->assertEquals(0, $cart->count());!
    }!
    }!

    View Slide

  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

    View Slide

  18. namespace Example;!
    class Cart!
    {!
    public function count()!
    {!
    return 0;!
    }!
    }!

    View Slide

  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)

    View Slide

  20. Business Requirement
    A customer should be able to add
    products to his or her cart.

    View Slide

  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());!
    }!
    }!

    View Slide

  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

    View Slide

  23. class Cart!
    {!
    //...snip...!
    public function addProduct($product)!
    {!
    !
    }!
    }!
    namespace Example;!
    class Product!
    {!
    !
    }!

    View Slide

  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.

    View Slide

  25. class Cart!
    {!
    private $contents;!
    !
    public function __construct()!
    {!
    $this->contents = array();!
    }!
    !
    //...snip...!
    !
    public function addProduct($product)!
    {!
    $this->contents[] = $product;!
    }!
    }!

    View Slide

  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.

    View Slide

  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;!
    }!
    }!

    View Slide

  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)

    View Slide

  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());!
    }!
    }!

    View Slide

  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());!
    }!
    }!

    View Slide

  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)

    View Slide

  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());!
    }!
    }!

    View Slide

  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));!
    }!
    }!

    View Slide

  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.

    View Slide

  35. class Cart implements \Countable!
    {!
    //...snip...!
    }!

    View Slide

  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)

    View Slide

  37. Use the tests cases you write to
    document your code’s behavior and
    roles for other developers–including
    your future self.
    Takeaway #3

    View Slide

  38. Business Requirement
    The subtotal for any given purchase is
    equivalent to the sum of the products’
    prices in a customer’s cart.

    View Slide

  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());!
    }!
    }!

    View Slide

  40. class Cart implements \Countable!
    {!
    //...snip...!
    !
    public function subtotal()!
    {!
    $runningTotal = 0;!
    foreach ($this->contents as $product) {!
    $runningTotal += $product->getPrice();!
    }!
    return $runningTotal;!
    }!
    }!

    View Slide

  41. class Product!
    {!
    private $price;!
    !
    public function __construct($price = 0)!
    {!
    $this->price = $price;!
    }!
    !
    public function getPrice()!
    {!
    return $this->price;!
    }!
    }!

    View Slide

  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)

    View Slide

  43. But...Unit Tests

    View Slide

  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());!
    }!
    }!

    View Slide

  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());!
    }!
    }!

    View Slide

  46. But...interfaces

    View Slide

  47. But...interfaces
    roles

    View Slide

  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());!
    }!
    }!

    View Slide

  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());!
    }!
    }!

    View Slide

  50. namespace Example;!
    interface Sellable!
    {!
    public function getPrice();!
    }!

    View Slide

  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)

    View Slide

  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;!
    }!
    }!

    View Slide

  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...!
    }!

    View Slide

  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)

    View Slide

  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());!
    }!
    }

    View Slide

  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;!
    }!
    }!

    View Slide

  57. Tip
    Type Hint on Roles Rather than Implementations
    public function addProduct(Sellable $product)!
    {!
    $this->contents[] = $product;!
    }!

    View Slide

  58. Business Requirement
    Promotions are allowed to be applied to
    a cart but can only apply to certain types
    of products.

    View Slide

  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());!
    }!
    }

    View Slide

  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;!
    }!
    }

    View Slide

  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)

    View Slide

  62. You can mock nonexistent
    interfaces to help you design the
    collaborators your object will use.
    Takeaway #4

    View Slide

  63. Recap
    4.

    View Slide

  64. Tests are
    sandboxes
    for your objects

    View Slide

  65. Focus on
    Communication

    View Slide

  66. Listen to
    your tests
    and refactor

    View Slide

  67. Further Reading

    View Slide

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

    View Slide

  69. Thank You
    @jcarouth
    joind.in/10825

    View Slide