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

Design how your objects talk through mocking

  • 1.
    Design how your objectstalk through mocking
  • 2.
    ” – Reverse Focuson 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.
    @everzet • BDD PracticeManager • Software Engineer • Creator of Behat, Mink, Prophecy, PhpSpec2 • Contributor to Symfony2, Doctrine2, Composer
  • 4.
    This talk isabout • 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.
  • 6.
    Money multiplication testfrom 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.
    Money multiplication testin 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.
    Event dispatching test publicfunction 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); }
  • 9.
    Event dispatching test publicfunction 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); }
  • 12.
    ” – Ralph WaldoEmerson “Life is a journey, not a destination.”
  • 13.
  • 14.
    Event dispatching test publicfunction 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); }
  • 15.
    Event dispatching collaborators publicfunction 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); }
  • 16.
    Find the message publicfunction 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); }
  • 17.
  • 18.
    ” – 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.”
  • 19.
    Interfaces interface LoginMessenger { publicfunction askForCard(); public function askForPin(); }   interface InputMessenger { public function askForAccount(); public function askForAmount(); }   interface WithdrawalMessenger { public function tellNoMoney(); public function tellMachineEmpty(); }
  • 20.
    Doubles 1. Dummy 2. Stub 3.Spy 4. Mock 5. Fake
  • 21.
    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();
  • 22.
  • 23.
    1. Dummy class System{ private $authorizer;   public function __construct(Authorizer $authorizer) { $this->authorizer = $authorizer; }   public function getLoginCount() { return 0; } }
  • 24.
    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()); }
  • 25.
    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()); }
  • 26.
  • 27.
    2. Stub class System{ // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } }
  • 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('everzet', ‘123’); ! $this->assertSame(1, $system->getLoginCount()); }
  • 29.
    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()); }
  • 30.
    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()); }
  • 31.
  • 32.
    3. Spy class System{ // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } }
  • 33.
    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(); }
  • 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.
  • 36.
    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(); }
  • 37.
    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(); }
  • 38.
  • 39.
    Find the message publicfunction 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); }
  • 40.
    Communication over state publicfunction 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(); }
  • 41.
    Exposed communication public functiontestEventIsDispatchedDuringRegistration() { $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(); }
  • 42.
  • 43.
    ” – The ObserverEffect “The act of observing will influence the phenomenon being observed.”
  • 44.
  • 45.
    Simple Symfony2 controller publicfunction 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 ]); }
  • 46.
    “Simple” Symfony2 controllertest 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.
    “Simple” Symfony2 controllertest 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); }
  • 48.
    “Simple” Symfony2 controllertest 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); }
  • 49.
  • 50.
    Simpler Symfony2 controllersimple 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); }
  • 51.
    Simpler Symfony2 controllersimple 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); }
  • 52.
    Simpler Symfony2 controller publicfunction maintainsPackagesAction(User $user) { $packages = $this->repo->getMaintainedPackagesOrderedByName($user); ! return $this->tpl->renderResponse('WebBundle:User:packages.html.twig', [ 'packages' => $packages ]); }
  • 53.
  • 54.
    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(); } }
  • 55.
    Basket checkout test publicfunction 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(); }
  • 56.
    Basket checkout testtwo 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(); }
  • 57.
    Basket checkout testtwo 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(); }
  • 58.
    Basket checkout duplicationin 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(); }
  • 59.
  • 60.
    Basket checkout testsimplification 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(); }
  • 61.
    Basket checkout testsimplification 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(); }
  • 62.
    Final basket checkout classBasket { // ...   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(); } }
  • 63.
    Final basket checkout classBasket { // ...   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(); } }
  • 64.
  • 65.
    Browser class Browser { publicfunction __construct(BrowserDriver $driver) { $this->driver = $driver; }   public function goto($url) { $this->driver->boot(); $this->driver->visit($url); } }
  • 66.
    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); } }
  • 67.
    Headless driver test publicfunction 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(); }
  • 68.
    Failing headless drivertest 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(); }
  • 69.
  • 70.
    Headless driver implementation classGuzzleDriver implements HeadlessBrowserDriver { ! public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }
  • 71.
    Headless driver simplebehaviour class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }
  • 72.
    Headless driver thatknows about booting class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() { $this->allowDoActions = true; }   public function visit($url) { if ($this->allowDoActions) $this->guzzle->openUrl($url); } }
  • 73.
  • 74.
    Adapter layer betweenBrowserDriver 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.
    Dirty adapter layerbetween 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); } }
  • 76.
    Single adapter layerbetween 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); } }
  • 77.
  • 78.
    ATM messenger interface interfaceMessenger { public function askForCard(); public function askForPin(); public function askForAccount(); public function askForAmount(); public function tellNoMoney(); public function tellMachineEmpty(); }
  • 79.
    City ATM logintest public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(Messenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }
  • 80.
    City ATM logintest public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(Messenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }
  • 81.
  • 82.
    City ATM logintest public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(LoginMessenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }
  • 83.
    ATM messenger interface(s) interfaceLoginMessenger { 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
  • 84.
  • 85.
    Doctrine entity repository classJobRepository extends EntityRepository { public function findJobByName($name) { return $this->findOneBy(['name' => $name]); } }
  • 86.
    Doctrine entity repositorytest 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); }
  • 87.
    Doctrine entity repositorytest 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.
    Do not mockthings you do not own
  • 89.
    Doctrine entity repositorytest 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); }
  • 90.
  • 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.
    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]);          }   }
  • 93.
    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]);          }   }
  • 94.
  • 95.
    Recap: 1. State-focused TDDis not the only way to TDD
  • 96.
    Recap: 1. State-focused TDDis not the only way to TDD 2. Messaging is far more important concept of OOP than the state
  • 97.
    Recap: 1. State-focused TDDis 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
  • 98.
    Recap: 1. State-focused TDDis 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
  • 99.
    Recap: 1. State-focused TDDis 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
  • 100.
  • 103.