Slide 1

Slide 1 text

Testing Essentials: Mock Objects Explained Jeff Carouth // @jcarouth // #zendcon

Slide 2

Slide 2 text

Definitions 1.

Slide 3

Slide 3 text

test double an object or procedure that looks and behaves like its release-intended counterpart, but is actually a simplified version that reduces the complexity and facilitates testing

Slide 4

Slide 4 text

stub a test double which provides “canned responses” to method calls on the stubbed object

Slide 5

Slide 5 text

mock a test double which sets and asserts pre-defined expectations of the methods it should or should not receive

Slide 6

Slide 6 text

spy a test double which records information about what was called and can be used to verify calls after the fact

Slide 7

Slide 7 text

fake a test double which is a simpler implementation of a real object

Slide 8

Slide 8 text

dummy a placeholder object that is not actually used in the object you are testing

Slide 9

Slide 9 text

There are technically-correct definitions and uses for the five types of test doubles you can create with most “mocking frameworks.” Takeaway #1

Slide 10

Slide 10 text

Why Mock? 2.

Slide 11

Slide 11 text

unit tests isolation require

Slide 12

Slide 12 text

unit tests isolation require from environment

Slide 13

Slide 13 text

unit tests isolation require from other tests

Slide 14

Slide 14 text

unit tests isolation require from external systems

Slide 15

Slide 15 text

unit tests isolation require from collaborators

Slide 16

Slide 16 text

The reason we mock is to isolate the unit of code we are intending to test from all dependencies including environment, external systems, tests, and other objects. Takeaway #2

Slide 17

Slide 17 text

How to Mock 3.

Slide 18

Slide 18 text

Test Dummy

Slide 19

Slide 19 text

getMock('SimpleDummyUser'); $this->assertInstanceOf('SimpleDummyUser', $dummyUser); } }

Slide 20

Slide 20 text

class UsingDummyUser { public $id; } class UsingDummyOrder { const STATUS_CANCELLED = 2; public $status; public function __construct(UsingDummyUser $user) { $this->user = $user; } public function cancel() { $this->status = self::STATUS_CANCELLED; } }

Slide 21

Slide 21 text

class UsingDummyTest extends \PHPUnit_Framework_TestCase { public function testCancelOrderMarksOrderCancelled() { $dummyUser = $this->getMock('UsingDummyUser'); $order = new UsingDummyOrder($dummyUser); $order->cancel(); $this->assertEquals( UsingDummyOrder::STATUS_CANCELLED, $order->status ); } }

Slide 22

Slide 22 text

Mocking Concrete Objects class DummyConstructorUser { public $id; public function __construct($id) { $this->id = $id; } }

Slide 23

Slide 23 text

class DummyConstructorTest extends PHPUnit_Framework_TestCase { public function testGetMockWithConstructorArguments() { $dummyUser = $this->getMock( 'DummyConstructorUser', // Class to mock null, // Methods to mock array(), // constructor params '', // mock Class name false // call the constructor? ); $this->assertInstanceOf('DummyConstructorUser', $dummyUser); } }

Slide 24

Slide 24 text

Tip Use PHPUnit’s getMockBuilder method public function testCleanedUpWithMockBuilder() { $dummyUser = $this->getMockBuilder('DummyConstructorUser') ->disableOriginalConstructor() ->getMock(); $this->assertInstanceOf('DummyConstructorUser', $dummyUser); }

Slide 25

Slide 25 text

Test Stub

Slide 26

Slide 26 text

class OrderProcessor { public function __construct(OrderDataSource $orderDataSource) { $this->orderDataSource = $orderDataSource; } public function cancelById($id) { $order = $this->orderDataSource->retrieve($id); if (false === $order) { return false; } $order['status'] = Order::STATUS_CANCELLED; return $this->orderDataSource->save($order); } }

Slide 27

Slide 27 text

class OrderDataSource { public function __construct(PDO $db) { $this->db = $db; } public function retrieve($id) { $sql = "SELECT * FROM orders WHERE id = ?"; $stmt = $this->db->prepare($sql); $stmt->execute(array($id)); return $stmt->fetch(PDO::FETCH_ASSOC); } public function save($order) { if (isset($order['id'])) { //update } else { //insert } } }

Slide 28

Slide 28 text

class OrderProcessorTest extends \PHPUnit_Framework_TestCase { public function testCancelByIdReturnsTrueOnSuccess() { $processor = new OrderProcessor( new OrderDataSource( new PDO('mysql:...', '...', '...', array()) ) ); $this->assertTrue($processor->cancelById(1)); } }

Slide 29

Slide 29 text

Real objects? No, stubs.

Slide 30

Slide 30 text

class StubOrderDataSource extends OrderDataSource { public function retrieve($id) { return array('id' => $id, 'total' => '47.99'); } public function save($data) { if (!isset($data['id'])) { return true; } else { return $data['id'] % 2 !== 0; } } }

Slide 31

Slide 31 text

class PDOTestHelper extends PDO { public function __construct() {} } class OrderProcessorTest extends \PHPUnit_Framework_TestCase { public function testCancelByIdReturnsTrueOnSuccess() { $processor = new OrderProcessor( new StubOrderDataSource( new PDOTestHelper() ) ); $this->assertTrue($processor->cancelById(1)); } }

Slide 32

Slide 32 text

public function testUsingMockFrameworkToStub() { $dataSource = $this->getMockBuilder('OrderDataSource') ->disableOriginalConstructor() ->getMock(); $dataSource->expects($this->any()) ->method('retrieve') ->will($this->returnValue( array('id' => 1, 'total' => 74.99)) ); $dataSource->expects($this->any()) ->method('save') ->will($this->returnValue(true)); $processor = new OrderProcessor($dataSource); $this->assertTrue($processor->cancelById(1)); }

Slide 33

Slide 33 text

Tip class UsingXpMockTest extends \PHPUnit_Framework_TestCase { use \Xpmock\TestCaseTrait; public function testUsingXpMock() { $dataSource = $this->mock('OrderDataSource') ->retrieve(array('id' => 1, 'total' => '74.99')) ->save(true) ->new(); $processor = new OrderProcessor($dataSource); $this->assertTrue($processor->cancelById(1)); } } http://crth.net/xpmock

Slide 34

Slide 34 text

Isolating code under test using dummies and stubs is easy to accomplish using a mocking framework. Takeaway #3

Slide 35

Slide 35 text

Fakes

Slide 36

Slide 36 text

class ComplicatedDataStore { public function fetch($id) { //interact with an API maybe? } public function save($row) { //interact with an API maybe? } }

Slide 37

Slide 37 text

class ComplicatedDataStoreConsumer { public function __construct(ComplicatedDataStore $source) { $this->source = $source; } public function createRecord($data) { //interact with $this->source } public function retrieveRecord($id) { //interact with $this->source } public function changeRecord($id, $param, $newValue) { //interact with $this->source } }

Slide 38

Slide 38 text

class FakeComplicatedDataStore extends ComplicatedDataStore { protected $rows = array(); public function fetch($id) { $id--; //decrement to map to array index if (isset($this->rows[$id])) { return $this->rows[$id]; } return null; } public function save($row) { if (!isset($row['id']) || $row['id'] == null) { $row['id'] = count($this->rows) + 1; } $this->rows[$row['id'] - 1] = $row; return $row['id']; } }

Slide 39

Slide 39 text

class UsingFakeTest extends \PHPUnit_Framework_TestCase { public function testUsingAFakeDataStoreBehavesLikeARealOne() { $consumer = new ComplicatedDataStoreConsumer( new FakeComplicatedDataStore() ); $record = array('name' => 'Test User'); $id = $consumer->createRecord($record); $this->assertTrue( $consumer->changeRecord($id, 'name', 'ATest User')); } }

Slide 40

Slide 40 text

Tip Always wrap external APIs and mock them in your tests. Remember unit tests must be isolated from external systems.

Slide 41

Slide 41 text

Mocks & Spies

Slide 42

Slide 42 text

class MailerService { public function sendConfirmation(Order $o) { mail($o->email, "Order#{$o->orderNumber}", "..."); } } class OrderProcessor { public function __construct(MailerService $m) { $this->emailer = $m; } public function processOrder(Order $order) { return $this->emailer->sendConfirmation($order); return true; } }

Slide 43

Slide 43 text

class MockingAndSpyingTest extends \PHPUnit_Framework_TestCase { public function testTheMockTellsMeWhenAMethodIsNotCalled() { $order = $this->getMockBuilder('Order') ->getMock(); $order->id = 1; $order->orderNumber = '111111'; $order->email = '[email protected]'; $emailer = $this->getMockBuilder('MailerService') ->disableOriginalConstructor() ->getMock(); $emailer->expects($this->once()) ->method('sendConfirmation') ->with($order) ->will($this->returnValue(true)); $processor = new OrderProcessor($emailer); $this->assertTrue($processor->processOrder($order)); } }

Slide 44

Slide 44 text

class OrderProcessor { public function __construct(MailerService $m) { $this->emailer = $m; } public function processOrder(Order $order) { //return $this->emailer->sendConfirmation($order); return true; } }

Slide 45

Slide 45 text

PHPUnit 3.7.27 by Sebastian Bergmann. F Time: 24 ms, Memory: 2.50Mb There was 1 failure: 1) MockingAndSpyingTest::testTheMockTellsMeWhenAMethodIsNotCalled Expectation failed for method name is equal to when invoked 1 time(s). Method was expected to be called 1 times, actually called 0 times. FAILURES! Tests: 1, Assertions: 2, Failures: 1.

Slide 46

Slide 46 text

Mocks and spies are useful for making sure your objects are working together as you intend. Takeaway #4

Slide 47

Slide 47 text

Recap 4.

Slide 48

Slide 48 text

unit tests isolation require

Slide 49

Slide 49 text

isolation is achieved with mocks

Slide 50

Slide 50 text

Companion Code http://crth.net/mockszc13

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

Thanks. http://speakerdeck.com/jcarouth Q&A http://joind.in/9086 Jeff Carouth // @jcarouth // #zendcon