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

Guiding Object-Oriented Design with Tests

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.

Jeff Carouth

December 20, 2013
Tweet

More Decks by Jeff Carouth

Other Decks in Programming

Transcript

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

    View full-size slide

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

    View full-size slide

  3. Traditional
    Development

    View full-size slide

  4. 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 full-size slide

  5. 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 full-size slide

  6. Communication
    over
    implementation

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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: Class 'Example\Cart' not found

    View full-size slide

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

    View full-size slide

  12. ! 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 full-size slide

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

    View full-size slide

  14. 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 full-size slide

  15. ! 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 full-size slide

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

    View full-size slide

  17. ! 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 full-size slide

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

    View full-size slide

  19. ! 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 full-size slide

  20. 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 full-size slide

  21. ! 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 full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

  24. ! 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 full-size slide

  25. 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 full-size slide

  26. 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 full-size slide

  27. ! 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 full-size slide

  28. class Cart implements \Countable
    {
    //...snip...
    }

    View full-size slide

  29. ! 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  32. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  35. ! 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 full-size slide

  36. But...Unit Tests

    View full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

  39. But...interfaces

    View full-size slide

  40. But...interfaces
    roles

    View full-size slide

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

    View full-size slide

  42. 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 full-size slide

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

    View full-size slide

  44. ! 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 full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. ! 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 full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

  54. ! 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 full-size slide

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

    View full-size slide

  56. Tests are
    sandboxes
    for your objects

    View full-size slide

  57. Focus on
    Communication

    View full-size slide

  58. Listen to
    your tests
    and refactor

    View full-size slide

  59. Further Reading

    View full-size slide

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

    View full-size slide

  61. JEFF CAROUTH
    DEVELOPER AT LIFTOPIA
    @jcarouth
    [email protected]
    Freenode: #phpmentoring

    View full-size slide