Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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.

Jeff Carouth

October 09, 2013
Tweet

More Decks by Jeff Carouth

Other Decks in Programming

Transcript

  1. 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
  2. mock a test double which sets and asserts pre-defined expectations

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

    called and can be used to verify calls after the fact
  4. There are technically-correct definitions and uses for the five types

    of test doubles you can create with most “mocking frameworks.” Takeaway #1
  5. 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
  6. <?php class SimpleDummyUser { public $id; } class SimpleDummyTest extends

    \PHPUnit_Framework_TestCase { public function testDummyWithPHPUnit() { $dummyUser = $this->getMock('SimpleDummyUser'); $this->assertInstanceOf('SimpleDummyUser', $dummyUser); } }
  7. 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; } }
  8. 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 ); } }
  9. 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); } }
  10. Tip Use PHPUnit’s getMockBuilder method public function testCleanedUpWithMockBuilder() { $dummyUser

    = $this->getMockBuilder('DummyConstructorUser') ->disableOriginalConstructor() ->getMock(); $this->assertInstanceOf('DummyConstructorUser', $dummyUser); }
  11. 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); } }
  12. 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 } } }
  13. class OrderProcessorTest extends \PHPUnit_Framework_TestCase { public function testCancelByIdReturnsTrueOnSuccess() { $processor

    = new OrderProcessor( new OrderDataSource( new PDO('mysql:...', '...', '...', array()) ) ); $this->assertTrue($processor->cancelById(1)); } }
  14. 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; } } }
  15. 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)); } }
  16. 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)); }
  17. 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
  18. Isolating code under test using dummies and stubs is easy

    to accomplish using a mocking framework. Takeaway #3
  19. class ComplicatedDataStore { public function fetch($id) { //interact with an

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

    tests. Remember unit tests must be isolated from external systems.
  24. 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; } }
  25. 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)); } }
  26. class OrderProcessor { public function __construct(MailerService $m) { $this->emailer =

    $m; } public function processOrder(Order $order) { //return $this->emailer->sendConfirmation($order); return true; } }
  27. 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.
  28. Mocks and spies are useful for making sure your objects

    are working together as you intend. Takeaway #4