Design how your objects talk through mocking

Post on 27-Aug-2014

682 Views

Category:

Software

1 Downloads

Preview:

Click to see full reader

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

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.”

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

Prophecy, PhpSpec2• Contributor to Symfony2,

Doctrine2, Composer

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

Test-driven development

By Example!

“The TDD book”!

Circa 2002

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); }

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()); }

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); }

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); }

”– Ralph Waldo Emerson

“Life is a journey, not a destination.”

Growing Object-Oriented

Software,Guided by Tests

!“The GOOS book”

!Circa 2009

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); }

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); }

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); }

messages or state

”– 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.”

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(); }

Doubles

1. Dummy

2. Stub

3. Spy

4. Mock

5. Fake

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();

1. Dummy

1. Dummy

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

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()); }

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()); }

2. Stub

2. Stub

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

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()); }

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()); }

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()); }

3. Spy

3. Spy

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

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(); }

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(); }

4. Mock

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(); }

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(); }

Back to the event dispatcher

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); }

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(); }

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(); }

Design?

”– The Observer Effect

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

The 1st case: simple controller

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 ]); }

“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); }

“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); }

“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); }

Single Responsibility Principle

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); }

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); }

Simpler Symfony2 controller

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

The 2nd case: basket checkout

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(); } }

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(); }

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(); }

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(); }

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(); }

Open Closed Principle

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(); }

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(); }

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(); } }

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(); } }

The 3rd case: browser emulation

Browser

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

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); } }

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(); }

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(); }

Refused Bequest

Headless driver implementation

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

Headless driver simple behaviour

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

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); } }

Liskov Substitution Principle

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); } }

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); } }

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); } }

The 4th case: ATM screen

ATM messenger interface

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

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(); }

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(); }

Interface Segregation Principle

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(); }

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

The 5th case: entity repository

Doctrine entity repository

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

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); }

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); }

Do not mock things you do not own

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); }

Dependency Inversion Principle

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]);          }  }

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]);          }  }

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]);          }  }

Recap:

Recap:

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

Recap:

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

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

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

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

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

6. Messages define objects behaviour

Thank you!

top related