Slide 1

Slide 1 text

Mocking With Mockery Ben Ramsey Midwest PHP Conference 5 March 2016

Slide 2

Slide 2 text

HI, I’M BEN. I’m a web craftsman, author, and speaker. I build a platform for professional photographers at ShootProof. I enjoy APIs, open source software, organizing user groups, good beer, and spending time with my family. Nashville, TN is my home. ▸Zend PHP Certification Study Guide ▸Nashville PHP & Atlanta PHP user groups ▸array_column() ▸ramsey/uuid ▸league/oauth2-client

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

Introduction to Mocking

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

INTRODUCTION TO MOCKING What is a mock object? ▸ Mock objects are a form of test double ▸ Test doubles are “any kind of pretend object used in place of a real object for testing purposes” (Martin Fowler) ▸ Mocks differ from other test doubles (like stubs) in that they are programmed with expectations about the calls they should receive ▸ Mocks are used in unit tests to replace behaviors of objects, services, etc. that are external to the current unit being tested but need to be called by it

Slide 7

Slide 7 text

INTRODUCTION TO MOCKING Mockery vs. PHPUnit ▸ Mockery provides a better user experience for working with mock objects, through an easy-to-use API ▸ Mockery provides abilities to mock things that PHPUnit can’t, like static methods and hard dependencies ▸ Mockery may be used together with PHPUnit or with any other testing framework

Slide 8

Slide 8 text

Getting Started With Mockery

Slide 9

Slide 9 text

GETTING STARTED Installing Mockery composer require mockery/mockery composer require phpunit/phpunit

Slide 10

Slide 10 text

GETTING STARTED namespace Ramsey\Talks; class Temperature { public function __construct($service) { $this->_service = $service; } public function average() { $total = 0; for ($i = 0; $i < 3; $i++) { $total += $this->_service->readTemp(); } return $total / 3; } }

Slide 11

Slide 11 text

GETTING STARTED namespace Ramsey\Talks; class Service { public function readTemp() { // Communicate with an external service and return // the current temperature. } }

Slide 12

Slide 12 text

GETTING STARTED $service = new \Ramsey\Talks\Service($params); $temperature = new \Ramsey\Talks\Temperature($service); echo $temperature->average();

Slide 13

Slide 13 text

GETTING STARTED namespace Ramsey\Talks\Test; class TemperatureTest extends \PHPUnit_Framework_TestCase { public function tearDown() { \Mockery::close(); } public function testGetsAverageTemperature() { $service = \Mockery::mock('servicemock'); $service->shouldReceive('readTemp') ->times(3) ->andReturn(10, 12, 14); $temperature = new \Ramsey\Talks\Temperature($service); $this->assertEquals(12, $temperature->average()); } }

Slide 14

Slide 14 text

GETTING STARTED Review ▸ A mock replaces an object that is expected to make certain calls ▸ \Mockery::mock('servicemock') creates a \Mockery\Mock object and is the loosest form of mock object ▸ Be sure to provide a tearDown() method in your tests that calls \Mockery::close(), to avoid problems

Slide 15

Slide 15 text

Mock Object Basics

Slide 16

Slide 16 text

MOCK OBJECT BASICS $mock = \Mockery::mock(['foo' => 1, 'bar' => 2]); $this->assertEquals(1, $mock->foo()); $this->assertEquals(2, $mock->bar());

Slide 17

Slide 17 text

MOCK OBJECT BASICS namespace Ramsey\Talks; class Temperature { public function __construct(Service $service) { $this->_service = $service; } public function average() { $total = 0; for ($i = 0; $i < 3; $i++) { $total += $this->_service->readTemp(); } return $total / 3; } }

Slide 18

Slide 18 text

MOCK OBJECT BASICS $service = \Mockery::mock('Ramsey\\Talks\\Service'); $service = \Mockery::mock('Ramsey\\Talks\\AbstractService'); $service = \Mockery::mock('Ramsey\\Talks\\ServiceInterface'); $service = \Mockery::mock( 'Ramsey\\Talks\\ServiceInterface, Countable, RecursiveIterator' );

Slide 19

Slide 19 text

MOCK OBJECT BASICS $mock = \Mockery::mock('classname', [ 'methodOne' => 'some return value', 'methodTwo' => 'another return value', 'methodThree' => 'yet another return value', ]); $this->assertEquals('some return value', $mock->methodOne()); $this->assertEquals('another return value', $mock->methodTwo()); $this->assertEquals('yet another return value', $mock->methodThree());

Slide 20

Slide 20 text

MOCK OBJECT BASICS Review ▸ Mockery allows you to define a named or unnamed mock object, naming all its methods and return values ▸ Mock objects can be type-hinted using a class, abstract class, or interface ▸ By default, any method called that is not defined will result in a BadMethodCallException; to return null instead, use the shouldIgnoreMissing() behavior modifier

Slide 21

Slide 21 text

Mock Expectations

Slide 22

Slide 22 text

MOCK EXPECTATIONS $service = \Mockery::mock('Ramsey\\Talks\\Service'); $service->shouldReceive('readTemp') ->times(3) ->andReturn(10, 12, 14); $service = \Mockery::mock('Ramsey\\Talks\\Service', [ 'readTemp' => 10 ]); We could have defined it like this: But then we couldn’t test the expectation that it should be called three times.

Slide 23

Slide 23 text

MOCK EXPECTATIONS namespace Ramsey\Talks; class Temperature { public function __construct($service) { $this->_service = $service; } public function average() { $total = 0; for ($i = 0; $i < 3; $i++) { $total += $this->_service->readTemp(); } return $total / 3; } }

Slide 24

Slide 24 text

MOCK EXPECTATIONS $service = \Mockery::mock('Ramsey\\Talks\\Service'); $service->shouldReceive('readTemp') ->times(3) ->andReturn(10, 12, 14);

Slide 25

Slide 25 text

MOCK EXPECTATIONS $mock = \Mockery::mock('Foo'); $mock->shouldReceive('methodCall') ->with('method', 'arg', 'values') ->andReturn(true);

Slide 26

Slide 26 text

MOCK EXPECTATIONS $mock->shouldReceive('methodCall') ->with('different', 'arg', 'values') ->andReturn(false);

Slide 27

Slide 27 text

MOCK EXPECTATIONS $mock->shouldReceive('methodCall') ->withNoArgs() ->andReturn(123);

Slide 28

Slide 28 text

MOCK EXPECTATIONS $this->assertFalse($mock->methodCall('different', 'arg', 'values')); $this->assertTrue($mock->methodCall('method', 'arg', 'values')); $this->assertEquals(123, $mock->methodCall());

Slide 29

Slide 29 text

MOCK EXPECTATIONS $user = \Mockery::mock('User'); $user->shouldReceive('getFriendById') ->andReturnUsing(function ($id) { // Do some special handling with the arguments here. // For example: $friendStub = file_get_contents("tests/stubs/friend{$id}.json"); return json_decode($friendStub); }); $friend = $user->getFriendById(1); $this->assertEquals('Jane Doe', $friend->name);

Slide 30

Slide 30 text

MOCK EXPECTATIONS /** * @expectedException RuntimeException * @expectedExceptionMessage An error occurred */ public function testServiceThrowsException() { $service = \Mockery::mock('Ramsey\\Talks\\Service'); $service->shouldReceive('readTemp') ->andThrow('RuntimeException', 'An error occurred'); $temperature = new \Ramsey\Talks\Temperature($service); $average = $temperature->average(); }

Slide 31

Slide 31 text

MOCK EXPECTATIONS Review ▸ Expectations on a mocked method affect its behavior depending on inputs and number of times called ▸ We covered times(), with(), withNoArgs(), andReturn(), andReturnUsing(), and andThrow(), but Mockery provides many more options

Slide 32

Slide 32 text

Partial Mocks

Slide 33

Slide 33 text

PARTIAL MOCKS $service = \Mockery::mock('Ramsey\\Talks\\Service[readTemp]'); $service->shouldReceive('readTemp') ->times(3) ->andReturn(10, 12, 14); $temperature = new \Ramsey\Talks\Temperature($service); $this->assertEquals(12, $temperature->average());

Slide 34

Slide 34 text

PARTIAL MOCKS $service = \Mockery::mock('Ramsey\\Talks\\Service[readTemp]', [ $constructorArg1, $constructorArg2, ]);

Slide 35

Slide 35 text

Mocking Final Classes

Slide 36

Slide 36 text

MOCKING FINAL CLASSES $staticUuid = 'dd39edd7-bb9c-414d-a7a0-78bd41edb4fb'; $uuid = \Mockery::mock('Ramsey\\Talks\\Uuid'); $uuid->shouldReceive('uuid4') ->andReturn($staticUuid); $this->assertEquals($staticUuid, $uuid->uuid4());

Slide 37

Slide 37 text

MOCKING FINAL CLASSES 1) Ramsey\Talks\Test\UserTest::testUuid Mockery\Exception: The class \Ramsey\Talks\Uuid is marked final and its methods cannot be replaced. Classes marked final can be passed in to \Mockery::mock() as instantiated objects to create a partial mock, but only if the mock is not subject to type hinting checks.

Slide 38

Slide 38 text

MOCKING FINAL CLASSES $staticUuid = 'dd39edd7-bb9c-414d-a7a0-78bd41edb4fb'; $uuidInstance = new \Ramsey\Talks\Uuid(); $uuid = \Mockery::mock($uuidInstance); $uuid->shouldReceive('uuid4') ->andReturn($staticUuid); $this->assertEquals($staticUuid, $uuid->uuid4()); This is referred to as a “proxied partial” mock.

Slide 39

Slide 39 text

Mocking Public Properties

Slide 40

Slide 40 text

MOCKING PUBLIC PROPERTIES $mock = \Mockery::mock('Foo'); $mock->publicProperty = 123; $this->assertEquals(123, $mock->publicProperty);

Slide 41

Slide 41 text

MOCKING PUBLIC PROPERTIES $mock = \Mockery::mock('Foo'); $mock->shouldReceive('methodCall') ->andSet('publicProperty', 123) ->andReturn(true); $this->assertTrue($mock->methodCall()); $this->assertEquals(123, $mock->publicProperty);

Slide 42

Slide 42 text

Mocking Fluent Interfaces

Slide 43

Slide 43 text

MOCKING FLUENT INTERFACES namespace Ramsey\Talks; class Bar { public function getSomething(Foo $foo) { $result = $foo->bar()->baz()->qux()->quux(); return "Now, we're {$result}"; } }

Slide 44

Slide 44 text

MOCKING FLUENT INTERFACES $mock = \Mockery::mock('Ramsey\\Talks\\Foo'); $mock->shouldReceive('bar->baz->qux->quux') ->andReturn('done!'); $bar = new \Ramsey\Talks\Bar; $this->assertEquals("Now, we're done!", $bar->getSomething($mock));

Slide 45

Slide 45 text

Mocking Static Methods

Slide 46

Slide 46 text

MOCKING STATIC METHODS namespace Ramsey\Talks; class User { public $addressId; public function getAddress() { return Address::getById($this->addressId); } }

Slide 47

Slide 47 text

MOCKING STATIC METHODS /** * @runInSeparateProcess * @preserveGlobalState disabled */ public function testGetAddress() { $address = \Mockery::mock('alias:Ramsey\\Talks\\Address'); $address->shouldReceive('getById') ->andReturn(new \Ramsey\Talks\Address()); $user = new \Ramsey\Talks\User(); $this->assertInstanceOf( 'Ramsey\\Talks\\Address', $user->getAddress() ); }

Slide 48

Slide 48 text

MOCKING STATIC METHODS 1) Ramsey\Talks\Test\UserTest::testGetAddress Mockery\Exception\RuntimeException: Could not load mock Ramsey\Talks\Address, class already exists

Slide 49

Slide 49 text

MOCKING STATIC METHODS /** * @runInSeparateProcess * @preserveGlobalState disabled */ public function testGetAddress() { $address = \Mockery::mock('alias:Ramsey\\Talks\\Address'); $address->shouldReceive('getById') ->andReturn(new \Ramsey\Talks\Address()); $user = new \Ramsey\Talks\User(); $this->assertInstanceOf( 'Ramsey\\Talks\\Address', $user->getAddress() ); }

Slide 50

Slide 50 text

Mocking Hard Dependencies

Slide 51

Slide 51 text

MOCKING HARD DEPENDENCIES namespace Ramsey\Talks; class User { public $addressId; public function getAddress() { return new Address($this->addressId); } }

Slide 52

Slide 52 text

MOCKING HARD DEPENDENCIES /** * @runInSeparateProcess * @preserveGlobalState disabled */ public function testGetAddress() { $address = \Mockery::mock('overload:Ramsey\\Talks\\Address'); $user = new \Ramsey\Talks\User(); $user->addressId = 123; $this->assertInstanceOf( 'Ramsey\\Talks\\Address', $user->getAddress() ); }

Slide 53

Slide 53 text

Wrapping Up

Slide 54

Slide 54 text

WRAPPING UP $foo = \Mockery::mock('Ramsey\\Talks\\Foo'); /* ... */ if ($foo instanceof \Mockery\MockInterface) { /* ... */ }

Slide 55

Slide 55 text

WRAPPING UP Review ▸ Mock objects are used to replace real objects in tests ▸ Mockery lets us create dumb mocks, mocks inherited from classes and interfaces, partial mocks, and aliases ▸ We saw how to use proxied partial mocks to mock final classes and methods ▸ We mocked public properties and fluent interfaces ▸ We created an aliased mock to mock a static method and an overloaded mock to instantiate instance mocks with the new keyword

Slide 56

Slide 56 text

THANK YOU. ANY QUESTIONS? If you want to talk more, feel free to contact me. benramsey.com @ramsey github.com/ramsey [email protected] Mocking With Mockery Copyright © 2016 Ben Ramsey This work is licensed under Creative Commons Attribution- ShareAlike 4.0 International. For uses not covered under this license, please contact the author. Ramsey, Ben. “Mocking With Mockery.” Midwest PHP Conference. Hilton Minneapolis, Minneapolis. 5 Mar. 2016. Conference presentation. This presentation was created using Keynote. The text is set in Chunk Five and Helvetica Neue. The source code is set in Menlo. The iconography is provided by Font Awesome. Unless otherwise noted, all photographs are used by permission under a Creative Commons license. Please refer to the Photo Credits slide for more information. joind.in/talk/e85df Ŏ

Slide 57

Slide 57 text

PHOTO CREDITS 1. “Mockingbird Silhouette” by Jim Mullhaupt 2. Untitled by Eli White 3. “Northern Mockingbird” by Kelly Colgan Azar 4. “Burla” by Daniel Lobo 5. “Mockingbird” by Neal Simpson 6. “Tropical Mockingbird” by hjhipster 7. “Mockingbird” by Henry T. McLin 8. “Northern Mockingbird (Cape May Point SP)” by Brian Henderson 9. “Northern Mockingbird” by Sandy/Chuck Harris 10.“Northern Mockingbird (Mimus polyglottos)” by Nicole Beaulac 11.“mockingbird in spring” by Melinda Shelton 12.“Mockingbird” by magpie_drain 13.“Mockingbird” by Kerry Lannert 14.“Northern Mockingbird” by Kelly Colgan Azar 1 2 3 4 5 6 7 8 9 10 11 12 13 14