Design how your objects talk through mocking

103
Design how your objects talk through mocking

description

What should you test with your unit tests? Some people will say that unit behaviour is best tested through it's outcomes. But what if communication between units itself is more important than the results of it? This session will introduce you to two different ways of unit-testing and show you a way to assert your object behaviours through their communications.

Transcript of Design how your objects talk through mocking

Page 1: Design how your objects talk through mocking

Design how your objects talk

through mocking

Page 2: Design how your objects talk through mocking

”– Reverse Focus on the reverse mortgages

“One of the most common mistakes people make is to fixate on the goal or expected outcome while ignoring their underlying

behaviours.”

Page 3: Design how your objects talk through mocking

@everzet• BDD Practice Manager• Software Engineer• Creator of Behat, Mink,

Prophecy, PhpSpec2• Contributor to Symfony2,

Doctrine2, Composer

Page 4: Design how your objects talk through mocking

This talk is about

• Test-driven development with and without mocks

• Introducing and making sense of different types of doubles

• OOP as a messaging paradigm

• Software design as a response to messaging observations

• Code

Page 5: Design how your objects talk through mocking

Test-driven development

By Example!

“The TDD book”!

Circa 2002

Page 6: Design how your objects talk through mocking

Money multiplication test from the TDD book

public void testMultiplication() { Dollar five = new Dollar(5); Dollar product = five.times(2); ! assertEquals(10, product.amount); ! product = five.times(3); ! assertEquals(15, product.amount); }

Page 7: Design how your objects talk through mocking

Money multiplication test in PHP

public function testMultiplication() { $five = new Dollar(5); $product = $five->times(2); $this->assertEquals(10, $product->getAmount()); $product = $five->times(3); $this->assertEquals(15, $product->getAmount()); }

Page 8: Design how your objects talk through mocking

Event dispatching test

public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('[email protected]');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }

Page 9: Design how your objects talk through mocking

Event dispatching test

public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('[email protected]');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }

Page 10: Design how your objects talk through mocking
Page 11: Design how your objects talk through mocking
Page 12: Design how your objects talk through mocking

”– Ralph Waldo Emerson

“Life is a journey, not a destination.”

Page 13: Design how your objects talk through mocking

Growing Object-Oriented

Software,Guided by Tests

!“The GOOS book”

!Circa 2009

Page 14: Design how your objects talk through mocking

Event dispatching test

public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('[email protected]');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }

Page 15: Design how your objects talk through mocking

Event dispatching collaborators

public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('[email protected]');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }

Page 16: Design how your objects talk through mocking

Find the message

public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('[email protected]');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }

Page 17: Design how your objects talk through mocking

messages or state

Page 18: Design how your objects talk through mocking

”– Alan Kay, father of OOP

“OOP to me means only messaging, local retention and protection and hiding of state-

process, and extreme late-binding of all things.”

Page 19: Design how your objects talk through mocking

Interfaces

interface LoginMessenger { public function askForCard(); public function askForPin(); }   interface InputMessenger { public function askForAccount(); public function askForAmount(); }   interface WithdrawalMessenger { public function tellNoMoney(); public function tellMachineEmpty(); }

Page 20: Design how your objects talk through mocking

Doubles

1. Dummy

2. Stub

3. Spy

4. Mock

5. Fake

Page 21: Design how your objects talk through mocking

Prophecy

(1) use Prophecy\Prophet;

(2) use Prophecy\Argument;

(3) $prophet = new Prophet();

(4) $userProphecy = $prophet->prophesize(UserInterface::class);

(5) $userProphecy->changeName('everzet')->shouldBeCalled();

(6) $user = $userProphecy->reveal();

(7) $user->changeName('_md');

(8) $prophet->checkPredictions();

Page 22: Design how your objects talk through mocking

1. Dummy

Page 23: Design how your objects talk through mocking

1. Dummy

class System { private $authorizer;   public function __construct(Authorizer $authorizer) { $this->authorizer = $authorizer; }   public function getLoginCount() { return 0; } }

Page 24: Design how your objects talk through mocking

1. Dummy

class System { private $authorizer;   public function __construct(Authorizer $authorizer) { $this->authorizer = $authorizer; }   public function getLoginCount() { return 0; } } !public function testNewlyCreatedSystemHasNoLoggedInUsers() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); $this->assertSame(0, $system->getLoginCount()); }

Page 25: Design how your objects talk through mocking

1. Dummy

class System { private $authorizer;   public function __construct(Authorizer $authorizer) { $this->authorizer = $authorizer; }   public function getLoginCount() { return 0; } } !public function testNewlyCreatedSystemHasNoLoggedInUsers() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); $this->assertSame(0, $system->getLoginCount()); }

Page 26: Design how your objects talk through mocking

2. Stub

Page 27: Design how your objects talk through mocking

2. Stub

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } }

Page 28: Design how your objects talk through mocking

2. Stub

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } } !public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); ! $auth->authorize('everzet', '123')->willReturn(true); ! $system->logIn('everzet', ‘123’); ! $this->assertSame(1, $system->getLoginCount()); }

Page 29: Design how your objects talk through mocking

2. Stub

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } } !public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); ! $auth->authorize('everzet', '123')->willReturn(true); ! $system->logIn('everzet', ‘123’); ! $this->assertSame(1, $system->getLoginCount()); }

Page 30: Design how your objects talk through mocking

2. Stub

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } } !public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); ! $auth->authorize('everzet', '123')->willReturn(true); ! $system->logIn(‘_md', ‘321’); ! $this->assertSame(1, $system->getLoginCount()); }

Page 31: Design how your objects talk through mocking

3. Spy

Page 32: Design how your objects talk through mocking

3. Spy

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } }

Page 33: Design how your objects talk through mocking

3. Spy

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } } !public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $timer = $this->prophesize(LoginTimer::class); $system = new System($auth->reveal(), $timer->reveal()); $auth->authorize('everzet', '123')->willReturn(true); ! $system->login('everzet', '123'); ! $timer->recordLogin('everzet')->shouldHaveBeenCalled(); }

Page 34: Design how your objects talk through mocking

3. Spy

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } } !public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $timer = $this->prophesize(LoginTimer::class); $system = new System($auth->reveal(), $timer->reveal()); $auth->authorize('everzet', '123')->willReturn(true); ! $system->login('everzet', '123'); ! $timer->recordLogin('everzet')->shouldHaveBeenCalled(); }

Page 35: Design how your objects talk through mocking

4. Mock

Page 36: Design how your objects talk through mocking

3. Spy

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } } !public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $timer = $this->prophesize(LoginTimer::class); $system = new System($auth->reveal(), $timer->reveal()); $auth->authorize('everzet', '123')->willReturn(true); ! $system->login('everzet', '123'); ! $timer->recordLogin('everzet')->shouldHaveBeenCalled(); }

Page 37: Design how your objects talk through mocking

4. Mock

class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } } !public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $timer = $this->prophesize(LoginTimer::class); $system = new System($auth->reveal(), $timer->reveal()); $auth->authorize('everzet', '123')->willReturn(true); ! $timer->recordLogin('everzet')->shouldBeCalled(); ! $system->login('everzet', '123'); ! $this->getProphet()->checkPredictions(); }

Page 38: Design how your objects talk through mocking

Back to the event dispatcher

Page 39: Design how your objects talk through mocking

Find the message

public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('[email protected]');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }

Page 40: Design how your objects talk through mocking

Communication over state

public function testEventIsDispatchedDuringRegistration() { $repository = $this->prophesize(UserRepository::class); $dispatcher = $this->prophesize(EventDispatcher::class); $manager = new UserManager( $repository->reveal(), $dispatcher->reveal() );   $user = User::signup('[email protected]');  $manager->registerUser($user); ! $dispatcher->dispatch('userIsRegistered', Argument::any()) ->shouldHaveBeenCalled(); }

Page 41: Design how your objects talk through mocking

Exposed communication

public function testEventIsDispatchedDuringRegistration() { $repository = $this->prophesize(UserRepository::class); $dispatcher = $this->prophesize(EventDispatcher::class); $manager = new UserManager( $repository->reveal(), $dispatcher->reveal() );   $user = User::signup('[email protected]');  $manager->registerUser($user); ! $dispatcher->dispatch('userIsRegistered', Argument::any()) ->shouldHaveBeenCalled(); }

Page 42: Design how your objects talk through mocking

Design?

Page 43: Design how your objects talk through mocking

”– The Observer Effect

“The act of observing will influence the phenomenon being observed.”

Page 44: Design how your objects talk through mocking

The 1st case: simple controller

Page 45: Design how your objects talk through mocking

Simple Symfony2 controller

public function packagesListAction(Request $req, User $user) { $packages = $this->getDoctrine() ->getRepository('WebBundle:Package') ->getFilteredQueryBuilder(array('maintainer' => $user->getId())) ->orderBy('p.name') ->getQuery() ->execute(); ! return $this->render('WebBundle:User:packages.html.twig', [ 'packages' => $packages ]); }

Page 46: Design how your objects talk through mocking

“Simple” Symfony2 controller test

public function testShowMaintainedPackages() { $request = new Request(); $user = new User('everzet'); $container = $this->prophesize(ContainerInterface::class); $doctrine = $this->prophesize(EntityManager::class); $repository = $this->prophesize(PackageRepository::class); $queryBuilder = $this->prophesize(QueryBuilder::class); $query = $this->prophesize(Query::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $templating = $this->prophesize(EngineInterface::class); $response = new Response('User packages'); ! $container->get('doctrine.orm')->willReturn($doctrine); $doctrine->getRepository('WebBundle:Package')->willReturn($repository); $repository->getFilteredQueryBuilder(['maintainer' => $user->getId()]) ->willReturn($queryBuilder); $queryBuilder->orderBy('p.name')->shouldBeCalled(); $queryBuilder->getQuery()->willReturn($query); $query->execute()->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController(); $controller->setContainer($container); $controllerResult = $controller->maintainsPackagesAction($request, $user); ! $this->assertEquals($response, $controllerResult); }

Page 47: Design how your objects talk through mocking

“Simple” Symfony2 controller test

public function testShowMaintainedPackages() { $request = new Request(); $user = new User('everzet'); $container = $this->prophesize(ContainerInterface::class); $doctrine = $this->prophesize(EntityManager::class); $repository = $this->prophesize(PackageRepository::class); $queryBuilder = $this->prophesize(QueryBuilder::class); $query = $this->prophesize(Query::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $templating = $this->prophesize(EngineInterface::class); $response = new Response('User packages'); ! $container->get('doctrine.orm')->willReturn($doctrine); $doctrine->getRepository('WebBundle:Package')->willReturn($repository); $repository->getFilteredQueryBuilder(['maintainer' => $user->getId()]) ->willReturn($queryBuilder); $queryBuilder->orderBy('p.name')->shouldBeCalled(); $queryBuilder->getQuery()->willReturn($query); $query->execute()->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController(); $controller->setContainer($container); $controllerResult = $controller->maintainsPackagesAction($request, $user); ! $this->assertEquals($response, $controllerResult); }

Page 48: Design how your objects talk through mocking

“Simple” Symfony2 controller test

public function testShowMaintainedPackages() { $request = new Request(); $user = new User('everzet'); $container = $this->prophesize(ContainerInterface::class); $doctrine = $this->prophesize(EntityManager::class); $repository = $this->prophesize(PackageRepository::class); $queryBuilder = $this->prophesize(QueryBuilder::class); $query = $this->prophesize(Query::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $templating = $this->prophesize(EngineInterface::class); $response = new Response('User packages'); ! $container->get('doctrine.orm')->willReturn($doctrine); $doctrine->getRepository('WebBundle:Package')->willReturn($repository); $repository->getFilteredQueryBuilder(['maintainer' => $user->getId()]) ->willReturn($queryBuilder); $queryBuilder->orderBy('p.name')->shouldBeCalled(); $queryBuilder->getQuery()->willReturn($query); $query->execute()->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController(); $controller->setContainer($container); $controllerResult = $controller->maintainsPackagesAction($request, $user); ! $this->assertEquals($response, $controllerResult); }

Page 49: Design how your objects talk through mocking

Single Responsibility Principle

Page 50: Design how your objects talk through mocking

Simpler Symfony2 controller simple test

public function testShowMaintainedPackages() { $user = new User('everzet'); $repository = $this->prophesize(PackageRepository::class); $templating = $this->prophesize(EngineInterface::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $response = new Response('User packages'); ! $repository->getMaintainedPackagesOrderedByName($user)->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController($repository->reveal(), $templating->reveal()); $controllerResult = $controller->maintainsPackagesAction($user); ! $this->assertEquals($response, $controllerResult); }

Page 51: Design how your objects talk through mocking

Simpler Symfony2 controller simple test

public function testShowMaintainedPackages() { $user = new User('everzet'); $repository = $this->prophesize(PackageRepository::class); $templating = $this->prophesize(EngineInterface::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $response = new Response('User packages'); ! $repository->getMaintainedPackagesOrderedByName($user)->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController($repository->reveal(), $templating->reveal()); $controllerResult = $controller->maintainsPackagesAction($user); ! $this->assertEquals($response, $controllerResult); }

Page 52: Design how your objects talk through mocking

Simpler Symfony2 controller

public function maintainsPackagesAction(User $user) { $packages = $this->repo->getMaintainedPackagesOrderedByName($user); ! return $this->tpl->renderResponse('WebBundle:User:packages.html.twig', [ 'packages' => $packages ]); }

Page 53: Design how your objects talk through mocking

The 2nd case: basket checkout

Page 54: Design how your objects talk through mocking

Basket checkout

class Basket { // ... ! public function checkout(OrderProcessor $processor) { $totalPrice = new Price::free(); foreach ($this->getItems() as $item) { $totalPrice = $totalPrice->add($item->getPrice()); $processor->addItem($item); } ! $payment = new CashPayment::fromPrice($totalPrice); $processor->setPayment($payment);   $processor->pay(); } }

Page 55: Design how your objects talk through mocking

Basket checkout test

public function testCheckout() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::which('getPrice', 15))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }

Page 56: Design how your objects talk through mocking

Basket checkout test two payments

public function testCheckoutWithCash() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = false); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CashPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }   public function testCheckoutWithCreditCard() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = true); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CreditPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }

Page 57: Design how your objects talk through mocking

Basket checkout test two payments

public function testCheckoutWithCash() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = false); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CashPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }   public function testCheckoutWithCreditCard() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = true); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CreditPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }

Page 58: Design how your objects talk through mocking

Basket checkout duplication in test

public function testCheckoutWithCash() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = false); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CashPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }   public function testCheckoutWithCreditCard() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = true); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CreditPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }

Page 59: Design how your objects talk through mocking

Open Closed Principle

Page 60: Design how your objects talk through mocking

Basket checkout test simplification

public function testCheckoutWithPaymentMethod() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class); $paymentMethod = $this->prophesize(PaymentMethod::class); $payment = $this->prophesize(Payment::class);   $paymentMethod->acceptPayment(Price::fromInt(15))->willReturn($payment);   $basket = new Basket($items); $basket->checkout($processor->reveal(), $paymentMethod->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment($payment)->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }

Page 61: Design how your objects talk through mocking

Basket checkout test simplification

public function testCheckoutWithPaymentMethod() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class); $paymentMethod = $this->prophesize(PaymentMethod::class); $payment = $this->prophesize(Payment::class);   $paymentMethod->acceptPayment(Price::fromInt(15))->willReturn($payment);   $basket = new Basket($items); $basket->checkout($processor->reveal(), $paymentMethod->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment($payment)->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }

Page 62: Design how your objects talk through mocking

Final basket checkout

class Basket { // ...   public function checkout(OrderProcessor $processor, PaymentMethod $method) { $totalPrice = new Price::free(); foreach ($this->getItems() as $item) { $totalPrice = $totalPrice->add($item->getPrice()); $processor->addItem($item); }   $payment = $method->acceptPayment($totalPrice); $processor->setPayment($payment);   $processor->pay(); } }

Page 63: Design how your objects talk through mocking

Final basket checkout

class Basket { // ...   public function checkout(OrderProcessor $processor, PaymentMethod $method) { $totalPrice = new Price::free(); foreach ($this->getItems() as $item) { $totalPrice = $totalPrice->add($item->getPrice()); $processor->addItem($item); }   $payment = $method->acceptPayment($totalPrice); $processor->setPayment($payment);   $processor->pay(); } }

Page 64: Design how your objects talk through mocking

The 3rd case: browser emulation

Page 65: Design how your objects talk through mocking

Browser

class Browser { public function __construct(BrowserDriver $driver) { $this->driver = $driver; }   public function goto($url) { $this->driver->boot(); $this->driver->visit($url); } }

Page 66: Design how your objects talk through mocking

Browser drivers

interface BrowserDriver { public function boot(); public function visit($url); } !interface HeadlessBrowserDriver extends BrowserDriver {} !class SeleniumDriver implements BrowserDriver { public function boot() { $this->selenium->startBrowser($this->browser); } ! public function visit($url) { $this->selenium->visitUrl($url); } } !class GuzzleDriver implements HeadlessBrowserDriver { public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }

Page 67: Design how your objects talk through mocking

Headless driver test

public function testVisitingProvidedUrl() { $url = 'http://en.wikipedia.org'; $driver = $this->prophesize(HeadlessBrowserDriver::class); ! $driver->visit($url)->shouldBeCalled(); ! $browser = new Browser($driver->reveal()); $browser->goto($url); ! $this->getProphecy()->checkPredictions(); }

Page 68: Design how your objects talk through mocking

Failing headless driver test

public function testVisitingProvidedUrl() { $url = 'http://en.wikipedia.org'; $driver = $this->prophesize(HeadlessBrowserDriver::class); ! $driver->visit($url)->shouldBeCalled(); ! $browser = new Browser($driver->reveal()); $browser->goto($url); ! $this->getProphecy()->checkPredictions(); }

Page 69: Design how your objects talk through mocking

Refused Bequest

Page 70: Design how your objects talk through mocking

Headless driver implementation

class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }

Page 71: Design how your objects talk through mocking

Headless driver simple behaviour

class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }

Page 72: Design how your objects talk through mocking

Headless driver that knows about booting

class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() { $this->allowDoActions = true; }   public function visit($url) { if ($this->allowDoActions) $this->guzzle->openUrl($url); } }

Page 73: Design how your objects talk through mocking

Liskov Substitution Principle

Page 74: Design how your objects talk through mocking

Adapter layer between BrowserDriver and HeadlessBrowserDriver

interface HeadlessBrowserDriver { public function visit($url); } !class GuzzleDriver implements HeadlessBrowserDriver { public function visit($url) { $this->guzzle->openUrl($url); } } !final class HeadlessBrowserAdapter implements BrowserDriver { private $headlessDriver, $allowDoAction = false; ! public function __construct(HeadlessBrowserDriver $headlessDriver) { $this->headlessDriver = $headlessDriver; } ! public function boot() { $this->allowDoActions = true; } ! public function visit($url) { if ($this->allowDoActions) $this->headlessDriver->visit($url); } }

Page 75: Design how your objects talk through mocking

Dirty adapter layer between BrowserDriver and HeadlessBrowserDriver

interface HeadlessBrowserDriver { public function visit($url); } !class GuzzleDriver implements HeadlessBrowserDriver { public function visit($url) { $this->guzzle->openUrl($url); } } !final class HeadlessBrowserAdapter implements BrowserDriver { private $headlessDriver, $allowDoAction = false; ! public function __construct(HeadlessBrowserDriver $headlessDriver) { $this->headlessDriver = $headlessDriver; } ! public function boot() { $this->allowDoActions = true; } ! public function visit($url) { if ($this->allowDoActions) $this->headlessDriver->visit($url); } }

Page 76: Design how your objects talk through mocking

Single adapter layer between BrowserDriver and HeadlessBrowserDriver

interface HeadlessBrowserDriver { public function visit($url); } !class GuzzleDriver implements HeadlessBrowserDriver { public function visit($url) { $this->guzzle->openUrl($url); } } !final class HeadlessBrowserAdapter implements BrowserDriver { private $headlessDriver, $allowDoAction = false; ! public function __construct(HeadlessBrowserDriver $headlessDriver) { $this->headlessDriver = $headlessDriver; } ! public function boot() { $this->allowDoActions = true; } ! public function visit($url) { if ($this->allowDoActions) $this->headlessDriver->visit($url); } }

Page 77: Design how your objects talk through mocking

The 4th case: ATM screen

Page 78: Design how your objects talk through mocking

ATM messenger interface

interface Messenger { public function askForCard(); public function askForPin(); public function askForAccount(); public function askForAmount(); public function tellNoMoney(); public function tellMachineEmpty(); }

Page 79: Design how your objects talk through mocking

City ATM login test

public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(Messenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }

Page 80: Design how your objects talk through mocking

City ATM login test

public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(Messenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }

Page 81: Design how your objects talk through mocking

Interface Segregation Principle

Page 82: Design how your objects talk through mocking

City ATM login test

public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(LoginMessenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }

Page 83: Design how your objects talk through mocking

ATM messenger interface(s)

interface LoginMessenger { public function askForCard(); public function askForPin(); } !interface InputMessenger { public function askForAccount(); public function askForAmount(); } !interface WithdrawalMessenger { public function tellNoMoney(); public function tellMachineEmpty(); } !interface Messenger extends LoginMessenger, InputMessenger, WithdrawalMessenger

Page 84: Design how your objects talk through mocking

The 5th case: entity repository

Page 85: Design how your objects talk through mocking

Doctrine entity repository

class JobRepository extends EntityRepository { public function findJobByName($name) { return $this->findOneBy(['name' => $name]); } }

Page 86: Design how your objects talk through mocking

Doctrine entity repository test

public function testFindingJobsByName() { $em = $this->prophesize('EntityManager'); $cmd = $this->prophesize('ClassMetadata'); $uow = $this->prophesize('UnitOfWork'); $ep = $this->prophesize('EntityPersister'); $job = Job::fromName('engineer'); ! $em->getUnitOfWork()->willReturn($uow); $uow->getEntityPersister(Argument::any())->willReturn($ep); $ep->load(['name' => 'engineer'], null, null, [], null, 1, null) ->willReturn($job); ! $repo = new JobRepository($em->reveal(), $cmd->reveal()); $actualJob = $repo->findJobByName('engineer'); ! $this->assertSame($job, $actualJob); }

Page 87: Design how your objects talk through mocking

Doctrine entity repository test

public function testFindingJobsByName() { $em = $this->prophesize('EntityManager'); $cmd = $this->prophesize('ClassMetadata'); $uow = $this->prophesize('UnitOfWork'); $ep = $this->prophesize('EntityPersister'); $job = Job::fromName('engineer'); ! $em->getUnitOfWork()->willReturn($uow); $uow->getEntityPersister(Argument::any())->willReturn($ep); $ep->load(['name' => 'engineer'], null, null, [], null, 1, null) ->willReturn($job); ! $repo = new JobRepository($em->reveal(), $cmd->reveal()); $actualJob = $repo->findJobByName('engineer'); ! $this->assertSame($job, $actualJob); }

Page 88: Design how your objects talk through mocking

Do not mock things you do not own

Page 89: Design how your objects talk through mocking

Doctrine entity repository test

public function testFindingJobsByName() { $em = $this->prophesize('EntityManager'); $cmd = $this->prophesize('ClassMetadata'); $uow = $this->prophesize('UnitOfWork'); $ep = $this->prophesize('EntityPersister'); $job = Job::fromName('engineer'); ! $em->getUnitOfWork()->willReturn($uow); $uow->getEntityPersister(Argument::any())->willReturn($ep); $ep->load(['name' => 'engineer'], null, null, [], null, 1, null) ->willReturn($job); ! $repo = new JobRepository($em->reveal(), $cmd->reveal()); $actualJob = $repo->findJobByName('engineer'); ! $this->assertSame($job, $actualJob); }

Page 90: Design how your objects talk through mocking

Dependency Inversion Principle

Page 91: Design how your objects talk through mocking

Job repository & Doctrine implementation of it

interface  JobRepository  {          public  function  findJobByName($name);  }  !class  DoctrineJobRepository  extends  EntityRepository                                                          implements  JobRepository  {  !        public  function  findJobByName($name)  {                  return  $this-­‐>findOneBy(['name'  =>  $name]);          }  }

Page 92: Design how your objects talk through mocking

Job repository & Doctrine implementation of it

interface  JobRepository  {          public  function  findJobByName($name);  }  !class  DoctrineJobRepository  extends  EntityRepository                                                          implements  JobRepository  {  !        public  function  findJobByName($name)  {                  return  $this-­‐>findOneBy(['name'  =>  $name]);          }  }

Page 93: Design how your objects talk through mocking

Job repository & Doctrine implementation of it

interface  JobRepository  {          public  function  findJobByName($name);  }  !class  DoctrineJobRepository  extends  EntityRepository                                                          implements  JobRepository  {  !        public  function  findJobByName($name)  {                  return  $this-­‐>findOneBy(['name'  =>  $name]);          }  }

Page 94: Design how your objects talk through mocking

Recap:

Page 95: Design how your objects talk through mocking

Recap:

1. State-focused TDD is not the only way to TDD

Page 96: Design how your objects talk through mocking

Recap:

1. State-focused TDD is not the only way to TDD

2. Messaging is far more important concept of OOP than the state

Page 97: Design how your objects talk through mocking

Recap:

1. State-focused TDD is not the only way to TDD

2. Messaging is far more important concept of OOP than the state

3. By focusing on messaging, you expose messaging problems

Page 98: Design how your objects talk through mocking

Recap:

1. State-focused TDD is not the only way to TDD

2. Messaging is far more important concept of OOP than the state

3. By focusing on messaging, you expose messaging problems

4. By exposing messaging problems, you could discover most of the SOLID principles violation before they happen

Page 99: Design how your objects talk through mocking

Recap:

1. State-focused TDD is not the only way to TDD

2. Messaging is far more important concept of OOP than the state

3. By focusing on messaging, you expose messaging problems

4. By exposing messaging problems, you could discover most of the SOLID principles violation before they happen

5. Prophecy is awesome

Page 100: Design how your objects talk through mocking

6. Messages define objects behaviour

Page 101: Design how your objects talk through mocking
Page 102: Design how your objects talk through mocking
Page 103: Design how your objects talk through mocking

Thank you!