Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

“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

Slide 4

Slide 4 text

“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

Slide 5

Slide 5 text

Why Test? 1.

Slide 6

Slide 6 text

Software Development Learning Feedback the process of requires which requires

Slide 7

Slide 7 text

Traditional Development

Slide 8

Slide 8 text

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.

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

OOP/OOD 2.

Slide 11

Slide 11 text

! Communication over implementation

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

TDD & OOP 3.

Slide 15

Slide 15 text

The Problem

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

! 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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

class CartTest extends \PHPUnit_Framework_TestCase! {! //...snip...! ! public function testCanAddOneProductToCart()! {! $cart = new \Example\Cart();! $cart->addProduct(new \Example\Product());! $this->assertEquals(1, $cart->count());! }! }!

Slide 22

Slide 22 text

! 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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

class Cart! {! private $contents;! ! public function __construct()! {! $this->contents = array();! }! ! public function count()! {! return count($this->contents);! }! ! public function addProduct($product)! {! $this->contents[] = $product;! }! }!

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

But...Unit Tests

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

But...interfaces

Slide 47

Slide 47 text

But...interfaces roles

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

class CartTest extends \PHPUnit_Framework_TestCase! {! //...snip...! ! public function testCanAddOneProductToCart()! {! $this->cart->addProduct($this->product(500, false));! $this->assertEquals(1, count($this->cart));! }! ! //...snip...! }!

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Recap 4.

Slide 64

Slide 64 text

Tests are sandboxes for your objects

Slide 65

Slide 65 text

Focus on Communication

Slide 66

Slide 66 text

Listen to your tests and refactor

Slide 67

Slide 67 text

Further Reading

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Thank You @jcarouth joind.in/10825