Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

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

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

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

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

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

    = array(); } public function count() { return count($this->contents); } 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 .. Time: 48 ms, Memory: 4.00Mb OK (2 tests, 2 assertions)
  14. 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()); } }
  15. 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()); } }
  16. ! 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)
  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. 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)); } }
  19. ! 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.
  20. ! 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)
  21. Use the tests cases you write to document your code’s

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

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

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

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

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

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

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

    collaborators your object will use. Takeaway #4