Design how your objects talk through mocking

4,771 views

Published on

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.

Published in: Software, Technology, Education
0 Comments
31 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
4,771
On SlideShare
0
From Embeds
0
Number of Embeds
119
Actions
Shares
0
Downloads
59
Comments
0
Likes
31
Embeds 0
No embeds

No notes for slide

Design how your objects talk through mocking

  1. 1. Design how your objects talk through mocking
  2. 2. ” – 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.”
  3. 3. @everzet • BDD Practice Manager • Software Engineer • Creator of Behat, Mink, Prophecy, PhpSpec2 • Contributor to Symfony2, Doctrine2, Composer
  4. 4. 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
  5. 5. Test-driven development By Example ! “The TDD book” ! Circa 2002
  6. 6. 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); }
  7. 7. 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()); }
  8. 8. 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('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  9. 9. 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('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  10. 10. ” – Ralph Waldo Emerson “Life is a journey, not a destination.”
  11. 11. Growing Object-Oriented Software, Guided by Tests ! “The GOOS book” ! Circa 2009
  12. 12. 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('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  13. 13. 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('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  14. 14. 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('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  15. 15. messages or state
  16. 16. ” – 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.”
  17. 17. 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(); }
  18. 18. Doubles 1. Dummy 2. Stub 3. Spy 4. Mock 5. Fake
  19. 19. Prophecy (1) use ProphecyProphet; (2) use ProphecyArgument; (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();
  20. 20. 1. Dummy
  21. 21. 1. Dummy class System { private $authorizer;   public function __construct(Authorizer $authorizer) { $this->authorizer = $authorizer; }   public function getLoginCount() { return 0; } }
  22. 22. 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()); }
  23. 23. 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()); }
  24. 24. 2. Stub
  25. 25. 2. Stub class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } }
  26. 26. 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()); }
  27. 27. 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()); }
  28. 28. 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()); }
  29. 29. 3. Spy
  30. 30. 3. Spy class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } }
  31. 31. 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(); }
  32. 32. 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(); }
  33. 33. 4. Mock
  34. 34. 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(); }
  35. 35. 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(); }
  36. 36. Back to the event dispatcher
  37. 37. 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('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  38. 38. 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('ever.zet@gmail.com');  $manager->registerUser($user); ! $dispatcher->dispatch('userIsRegistered', Argument::any()) ->shouldHaveBeenCalled(); }
  39. 39. 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('ever.zet@gmail.com');  $manager->registerUser($user); ! $dispatcher->dispatch('userIsRegistered', Argument::any()) ->shouldHaveBeenCalled(); }
  40. 40. Design?
  41. 41. ” – The Observer Effect “The act of observing will influence the phenomenon being observed.”
  42. 42. The 1st case: simple controller
  43. 43. 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 ]); }
  44. 44. “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); }
  45. 45. “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); }
  46. 46. “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); }
  47. 47. Single Responsibility Principle
  48. 48. 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); }
  49. 49. 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); }
  50. 50. Simpler Symfony2 controller public function maintainsPackagesAction(User $user) { $packages = $this->repo->getMaintainedPackagesOrderedByName($user); ! return $this->tpl->renderResponse('WebBundle:User:packages.html.twig', [ 'packages' => $packages ]); }
  51. 51. The 2nd case: basket checkout
  52. 52. 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(); } }
  53. 53. 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(); }
  54. 54. 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(); }
  55. 55. 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(); }
  56. 56. 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(); }
  57. 57. Open Closed Principle
  58. 58. 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(); }
  59. 59. 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(); }
  60. 60. 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(); } }
  61. 61. 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(); } }
  62. 62. The 3rd case: browser emulation
  63. 63. Browser class Browser { public function __construct(BrowserDriver $driver) { $this->driver = $driver; }   public function goto($url) { $this->driver->boot(); $this->driver->visit($url); } }
  64. 64. 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); } }
  65. 65. 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(); }
  66. 66. 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(); }
  67. 67. Refused Bequest
  68. 68. Headless driver implementation class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }
  69. 69. Headless driver simple behaviour class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }
  70. 70. 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); } }
  71. 71. Liskov Substitution Principle
  72. 72. 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); } }
  73. 73. 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); } }
  74. 74. 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); } }
  75. 75. The 4th case: ATM screen
  76. 76. ATM messenger interface interface Messenger { public function askForCard(); public function askForPin(); public function askForAccount(); public function askForAmount(); public function tellNoMoney(); public function tellMachineEmpty(); }
  77. 77. 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(); }
  78. 78. 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(); }
  79. 79. Interface Segregation Principle
  80. 80. 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(); }
  81. 81. 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
  82. 82. The 5th case: entity repository
  83. 83. Doctrine entity repository class JobRepository extends EntityRepository { public function findJobByName($name) { return $this->findOneBy(['name' => $name]); } }
  84. 84. 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); }
  85. 85. 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); }
  86. 86. Do not mock things you do not own
  87. 87. 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); }
  88. 88. Dependency Inversion Principle
  89. 89. 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]);          }   }
  90. 90. 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]);          }   }
  91. 91. 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]);          }   }
  92. 92. Recap:
  93. 93. Recap: 1. State-focused TDD is not the only way to TDD
  94. 94. Recap: 1. State-focused TDD is not the only way to TDD 2. Messaging is far more important concept of OOP than the state
  95. 95. 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
  96. 96. 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
  97. 97. 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
  98. 98. 6. Messages define objects behaviour
  99. 99. Thank you!

×