ZendCon 2013 - Testing Essentials: Mock Objects Explained

ZendCon 2013 - Testing Essentials: Mock Objects Explained

One of the critical aspects of testing to understand is how to use test doubles. Mock objects is a subject that can be difficult to grasp and often developers find themselves feeling like they are mocking too much and testing too little. In this session we will cover test doubles with a focus on mock objects, explaining how they are used as well as why they are used. After this session you will have a renewed confidence in writing tests and be well on your way to writing effective unit tests or specs for your code.

0f930e13633535c1c4041e95b8881308?s=128

Jeff Carouth

October 09, 2013
Tweet

Transcript

  1. Testing Essentials: Mock Objects Explained Jeff Carouth // @jcarouth //

    #zendcon
  2. Definitions 1.

  3. 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
  4. stub a test double which provides “canned responses” to method

    calls on the stubbed object
  5. mock a test double which sets and asserts pre-defined expectations

    of the methods it should or should not receive
  6. spy a test double which records information about what was

    called and can be used to verify calls after the fact
  7. fake a test double which is a simpler implementation of

    a real object
  8. dummy a placeholder object that is not actually used in

    the object you are testing
  9. There are technically-correct definitions and uses for the five types

    of test doubles you can create with most “mocking frameworks.” Takeaway #1
  10. Why Mock? 2.

  11. unit tests isolation require

  12. unit tests isolation require from environment

  13. unit tests isolation require from other tests

  14. unit tests isolation require from external systems

  15. unit tests isolation require from collaborators

  16. 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
  17. How to Mock 3.

  18. Test Dummy

  19. <?php class SimpleDummyUser { public $id; } class SimpleDummyTest extends

    \PHPUnit_Framework_TestCase { public function testDummyWithPHPUnit() { $dummyUser = $this->getMock('SimpleDummyUser'); $this->assertInstanceOf('SimpleDummyUser', $dummyUser); } }
  20. 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; } }
  21. 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 ); } }
  22. Mocking Concrete Objects class DummyConstructorUser { public $id; public function

    __construct($id) { $this->id = $id; } }
  23. 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); } }
  24. Tip Use PHPUnit’s getMockBuilder method public function testCleanedUpWithMockBuilder() { $dummyUser

    = $this->getMockBuilder('DummyConstructorUser') ->disableOriginalConstructor() ->getMock(); $this->assertInstanceOf('DummyConstructorUser', $dummyUser); }
  25. Test Stub

  26. 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); } }
  27. 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 } } }
  28. class OrderProcessorTest extends \PHPUnit_Framework_TestCase { public function testCancelByIdReturnsTrueOnSuccess() { $processor

    = new OrderProcessor( new OrderDataSource( new PDO('mysql:...', '...', '...', array()) ) ); $this->assertTrue($processor->cancelById(1)); } }
  29. Real objects? No, stubs.

  30. 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; } } }
  31. 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)); } }
  32. 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)); }
  33. 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
  34. Isolating code under test using dummies and stubs is easy

    to accomplish using a mocking framework. Takeaway #3
  35. Fakes

  36. class ComplicatedDataStore { public function fetch($id) { //interact with an

    API maybe? } public function save($row) { //interact with an API maybe? } }
  37. 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 } }
  38. 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']; } }
  39. 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')); } }
  40. Tip Always wrap external APIs and mock them in your

    tests. Remember unit tests must be isolated from external systems.
  41. Mocks & Spies

  42. 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; } }
  43. class MockingAndSpyingTest extends \PHPUnit_Framework_TestCase { public function testTheMockTellsMeWhenAMethodIsNotCalled() { $order

    = $this->getMockBuilder('Order') ->getMock(); $order->id = 1; $order->orderNumber = '111111'; $order->email = 'test@example.net'; $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)); } }
  44. class OrderProcessor { public function __construct(MailerService $m) { $this->emailer =

    $m; } public function processOrder(Order $order) { //return $this->emailer->sendConfirmation($order); return true; } }
  45. 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 <string:sendConfirmation> when invoked 1 time(s). Method was expected to be called 1 times, actually called 0 times. FAILURES! Tests: 1, Assertions: 2, Failures: 1.
  46. Mocks and spies are useful for making sure your objects

    are working together as you intend. Takeaway #4
  47. Recap 4.

  48. unit tests isolation require

  49. isolation is achieved with mocks

  50. Companion Code http://crth.net/mockszc13

  51. JEFF CAROUTH DEVELOPER AT LIFTOPIA @jcarouth jcarouth@gmail.com Freenode: #phpmentoring

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