Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Why Test? 1.

Slide 3

Slide 3 text

Software Development Learning Feedback the process of requires which requires

Slide 4

Slide 4 text

Traditional Development

Slide 5

Slide 5 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 6

Slide 6 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 7

Slide 7 text

OOP/OOD 2.

Slide 8

Slide 8 text

Communication over implementation

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

TDD & OOP 3.

Slide 12

Slide 12 text

The Problem

Slide 13

Slide 13 text

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

Slide 14

Slide 14 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 15

Slide 15 text

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

Slide 16

Slide 16 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 17

Slide 17 text

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

Slide 18

Slide 18 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 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 F Fatal error: Call to undefined method Example\Cart::addProduct() Fatal error: Class 'Example\Product' not found

Slide 20

Slide 20 text

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

Slide 21

Slide 21 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 22

Slide 22 text

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

Slide 23

Slide 23 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 24

Slide 24 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 25

Slide 25 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 26

Slide 26 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 27

Slide 27 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 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: 47 ms, Memory: 4.00Mb OK (2 tests, 2 assertions)

Slide 29

Slide 29 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 30

Slide 30 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 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 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 32

Slide 32 text

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

Slide 33

Slide 33 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 34

Slide 34 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 35

Slide 35 text

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

Slide 36

Slide 36 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 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 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 40

Slide 40 text

But...Unit Tests

Slide 41

Slide 41 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 42

Slide 42 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 43

Slide 43 text

But...interfaces

Slide 44

Slide 44 text

But...interfaces roles

Slide 45

Slide 45 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 46

Slide 46 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 47

Slide 47 text

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

Slide 48

Slide 48 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 49

Slide 49 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 50

Slide 50 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 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: 66 ms, Memory: 3.75Mb OK (4 tests, 6 assertions)

Slide 52

Slide 52 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 53

Slide 53 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 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 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 57

Slide 57 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 58

Slide 58 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 59

Slide 59 text

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

Slide 60

Slide 60 text

Recap 4.

Slide 61

Slide 61 text

Tests are sandboxes for your objects

Slide 62

Slide 62 text

Focus on Communication

Slide 63

Slide 63 text

Listen to your tests and refactor

Slide 64

Slide 64 text

Further Reading

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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