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 expecte...
@everzet
• BDD Practice Manager

• Software Engineer

• Creator of Behat, Mink,
Prophecy, PhpSpec2

• Contributor to Symfo...
This talk is about
• Test-driven development with and without mocks

• Introducing and making sense of different types of
d...
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...
Money multiplication test in PHP
public function testMultiplication()
{
$five = new Dollar(5);
$product = $five->times(2);...
Event dispatching test
public function testEventIsDispatchedDuringRegistration()
{
$dispatcher = new EventDispatcher();
$r...
Event dispatching test
public function testEventIsDispatchedDuringRegistration()
{
$dispatcher = new EventDispatcher();
$r...
”
– 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();
$r...
Event dispatching collaborators
public function testEventIsDispatchedDuringRegistration()
{
$dispatcher = new EventDispatc...
Find the message
public function testEventIsDispatchedDuringRegistration()
{
$dispatcher = new EventDispatcher();
$reposit...
messages or state
”
– Alan Kay, father of OOP
“OOP to me means only messaging, local
retention and protection and hiding of state-
process, ...
Interfaces
interface LoginMessenger {
public function askForCard();
public function askForPin();
}
 
interface InputMessen...
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->pr...
1. Dummy
1. Dummy
class System {
private $authorizer;
 
public function __construct(Authorizer $authorizer) {
$this->authorizer = $...
1. Dummy
class System {
private $authorizer;
 
public function __construct(Authorizer $authorizer) {
$this->authorizer = $...
1. Dummy
class System {
private $authorizer;
 
public function __construct(Authorizer $authorizer) {
$this->authorizer = $...
2. Stub
2. Stub
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, ...
2. Stub
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, ...
2. Stub
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, ...
2. Stub
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, ...
3. Spy
3. Spy
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $...
3. Spy
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $...
3. Spy
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $...
4. Mock
3. Spy
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, $...
4. Mock
class System {
// ...
!
public function logIn($username, $password) {
if ($this->authorizer->authorize($username, ...
Back to the
event dispatcher
Find the message
public function testEventIsDispatchedDuringRegistration()
{
$dispatcher = new EventDispatcher();
$reposit...
Communication over state
public function testEventIsDispatchedDuringRegistration()
{
$repository = $this->prophesize(UserR...
Exposed communication
public function testEventIsDispatchedDuringRegistration()
{
$repository = $this->prophesize(UserRepo...
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()...
“Simple” Symfony2 controller test
public function testShowMaintainedPackages() {
$request = new Request();
$user = new Use...
“Simple” Symfony2 controller test
public function testShowMaintainedPackages() {
$request = new Request();
$user = new Use...
“Simple” Symfony2 controller test
public function testShowMaintainedPackages() {
$request = new Request();
$user = new Use...
Single
Responsibility
Principle
Simpler Symfony2 controller simple test
public function testShowMaintainedPackages() {
$user = new User('everzet');
$repos...
Simpler Symfony2 controller simple test
public function testShowMaintainedPackages() {
$user = new User('everzet');
$repos...
Simpler Symfony2 controller
public function maintainsPackagesAction(User $user) {
$packages = $this->repo->getMaintainedPa...
The 2nd case:
basket checkout
Basket checkout
class Basket {
// ...
!
public function checkout(OrderProcessor $processor) {
$totalPrice = new Price::fre...
Basket checkout test
public function testCheckout() {
$items = [new Item(Price::fromInt(10), Price::fromInt(5)];
$processo...
Basket checkout test two payments
public function testCheckoutWithCash() {
$items = [new Item(Price::fromInt(10), Price::f...
Basket checkout test two payments
public function testCheckoutWithCash() {
$items = [new Item(Price::fromInt(10), Price::f...
Basket checkout duplication in test
public function testCheckoutWithCash() {
$items = [new Item(Price::fromInt(10), Price:...
Open
Closed
Principle
Basket checkout test simplification
public function testCheckoutWithPaymentMethod() {
$items = [new Item(Price::fromInt(10)...
Basket checkout test simplification
public function testCheckoutWithPaymentMethod() {
$items = [new Item(Price::fromInt(10)...
Final basket checkout
class Basket {
// ...
 
public function checkout(OrderProcessor $processor, PaymentMethod $method) {...
Final basket checkout
class Basket {
// ...
 
public function checkout(OrderProcessor $processor, PaymentMethod $method) {...
The 3rd case:
browser emulation
Browser
class Browser {
public function __construct(BrowserDriver $driver) {
$this->driver = $driver;
}
 
public function ...
Browser drivers
interface BrowserDriver {
public function boot();
public function visit($url);
}
!
interface HeadlessBrows...
Headless driver test
public function testVisitingProvidedUrl() {
$url = 'http://en.wikipedia.org';
$driver = $this->prophe...
Failing headless driver test
public function testVisitingProvidedUrl() {
$url = 'http://en.wikipedia.org';
$driver = $this...
Refused Bequest
Headless driver implementation
class GuzzleDriver implements HeadlessBrowserDriver {
!
public function boot() {}
!
public ...
Headless driver simple behaviour
class GuzzleDriver implements HeadlessBrowserDriver {
!
public function boot() {}
!
publi...
Headless driver that knows about booting
class GuzzleDriver implements HeadlessBrowserDriver {
!
public function boot() {
...
Liskov
Substitution
Principle
Adapter layer between BrowserDriver and HeadlessBrowserDriver
interface HeadlessBrowserDriver {
public function visit($url...
Dirty adapter layer between BrowserDriver and HeadlessBrowserDriver
interface HeadlessBrowserDriver {
public function visi...
Single adapter layer between BrowserDriver and HeadlessBrowserDriver
interface HeadlessBrowserDriver {
public function vis...
The 4th case:
ATM screen
ATM messenger interface
interface Messenger {
public function askForCard();
public function askForPin();
public function a...
City ATM login test
public function testAtmAsksForCardAndPinDuringLogin() {
$messenger = $this->prophesize(Messenger::clas...
City ATM login test
public function testAtmAsksForCardAndPinDuringLogin() {
$messenger = $this->prophesize(Messenger::clas...
Interface
Segregation
Principle
City ATM login test
public function testAtmAsksForCardAndPinDuringLogin() {
$messenger = $this->prophesize(LoginMessenger:...
ATM messenger interface(s)
interface LoginMessenger {
public function askForCard();
public function askForPin();
}
!
inter...
The 5th case:
entity repository
Doctrine entity repository
class JobRepository extends EntityRepository {
public function findJobByName($name) {
return $t...
Doctrine entity repository test
public function testFindingJobsByName() {
$em = $this->prophesize('EntityManager');
$cmd =...
Doctrine entity repository test
public function testFindingJobsByName() {
$em = $this->prophesize('EntityManager');
$cmd =...
Do not mock things
you do not own
Doctrine entity repository test
public function testFindingJobsByName() {
$em = $this->prophesize('EntityManager');
$cmd =...
Dependency
Inversion
Principle
Job repository & Doctrine implementation of it
interface	
  JobRepository	
  {	
  
	
  	
  	
  	
  public	
  function	
  f...
Job repository & Doctrine implementation of it
interface	
  JobRepository	
  {	
  
	
  	
  	
  	
  public	
  function	
  f...
Job repository & Doctrine implementation of it
interface	
  JobRepository	
  {	
  
	
  	
  	
  	
  public	
  function	
  f...
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

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

...
6. Messages define
objects behaviour
Thank you!
Design how your objects talk through mocking
Design how your objects talk through mocking
Design how your objects talk through mocking
Design how your objects talk through mocking
Prochain SlideShare
Chargement dans…5
×

Design how your objects talk through mocking

4 494 vues

Publié le

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.

Publié dans : Logiciels, Technologie, Formation
0 commentaire
30 j’aime
Statistiques
Remarques
  • Soyez le premier à commenter

Aucun téléchargement
Vues
Nombre de vues
4 494
Sur SlideShare
0
Issues des intégrations
0
Intégrations
108
Actions
Partages
0
Téléchargements
53
Commentaires
0
J’aime
30
Intégrations 0
Aucune incorporation

Aucune remarque pour cette diapositive

Design how your objects talk through mocking

  1. 1. Design how your objects talk through mocking
  2. 2. ” – Reverse Focus on the reverse mortgages “One of the most common mistakes people make is to fixate on the goal or expected outcome while ignoring their underlying behaviours.”
  3. 3. @everzet • BDD Practice Manager • Software Engineer • Creator of Behat, Mink, Prophecy, PhpSpec2 • Contributor to Symfony2, Doctrine2, Composer
  4. 4. This talk is about • Test-driven development with and without mocks • Introducing and making sense of different types of doubles • OOP as a messaging paradigm • Software design as a response to messaging observations • Code
  5. 5. Test-driven development By Example ! “The TDD book” ! Circa 2002
  6. 6. Money multiplication test from the TDD book public void testMultiplication() { Dollar five = new Dollar(5); Dollar product = five.times(2); ! assertEquals(10, product.amount); ! product = five.times(3); ! assertEquals(15, product.amount); }
  7. 7. Money multiplication test in PHP public function testMultiplication() { $five = new Dollar(5); $product = $five->times(2); $this->assertEquals(10, $product->getAmount()); $product = $five->times(3); $this->assertEquals(15, $product->getAmount()); }
  8. 8. Event dispatching test public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  9. 9. Event dispatching test public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  10. 10. ” – Ralph Waldo Emerson “Life is a journey, not a destination.”
  11. 11. Growing Object-Oriented Software, Guided by Tests ! “The GOOS book” ! Circa 2009
  12. 12. Event dispatching test public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  13. 13. Event dispatching collaborators public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  14. 14. Find the message public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  15. 15. messages or state
  16. 16. ” – Alan Kay, father of OOP “OOP to me means only messaging, local retention and protection and hiding of state- process, and extreme late-binding of all things.”
  17. 17. Interfaces interface LoginMessenger { public function askForCard(); public function askForPin(); }   interface InputMessenger { public function askForAccount(); public function askForAmount(); }   interface WithdrawalMessenger { public function tellNoMoney(); public function tellMachineEmpty(); }
  18. 18. Doubles 1. Dummy 2. Stub 3. Spy 4. Mock 5. Fake
  19. 19. Prophecy (1) use ProphecyProphet; (2) use ProphecyArgument; (3) $prophet = new Prophet(); (4) $userProphecy = $prophet->prophesize(UserInterface::class); (5) $userProphecy->changeName('everzet')->shouldBeCalled(); (6) $user = $userProphecy->reveal(); (7) $user->changeName('_md'); (8) $prophet->checkPredictions();
  20. 20. 1. Dummy
  21. 21. 1. Dummy class System { private $authorizer;   public function __construct(Authorizer $authorizer) { $this->authorizer = $authorizer; }   public function getLoginCount() { return 0; } }
  22. 22. 1. Dummy class System { private $authorizer;   public function __construct(Authorizer $authorizer) { $this->authorizer = $authorizer; }   public function getLoginCount() { return 0; } } ! public function testNewlyCreatedSystemHasNoLoggedInUsers() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); $this->assertSame(0, $system->getLoginCount()); }
  23. 23. 1. Dummy class System { private $authorizer;   public function __construct(Authorizer $authorizer) { $this->authorizer = $authorizer; }   public function getLoginCount() { return 0; } } ! public function testNewlyCreatedSystemHasNoLoggedInUsers() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); $this->assertSame(0, $system->getLoginCount()); }
  24. 24. 2. Stub
  25. 25. 2. Stub class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } }
  26. 26. 2. Stub class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } } ! public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); ! $auth->authorize('everzet', '123')->willReturn(true); ! $system->logIn('everzet', ‘123’); ! $this->assertSame(1, $system->getLoginCount()); }
  27. 27. 2. Stub class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } } ! public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); ! $auth->authorize('everzet', '123')->willReturn(true); ! $system->logIn('everzet', ‘123’); ! $this->assertSame(1, $system->getLoginCount()); }
  28. 28. 2. Stub class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; } } ! public function getLoginCount() { return $this->loginCount; } } ! public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $system = new System($auth->reveal()); ! $auth->authorize('everzet', '123')->willReturn(true); ! $system->logIn(‘_md', ‘321’); ! $this->assertSame(1, $system->getLoginCount()); }
  29. 29. 3. Spy
  30. 30. 3. Spy class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } }
  31. 31. 3. Spy class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } } ! public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $timer = $this->prophesize(LoginTimer::class); $system = new System($auth->reveal(), $timer->reveal()); $auth->authorize('everzet', '123')->willReturn(true); ! $system->login('everzet', '123'); ! $timer->recordLogin('everzet')->shouldHaveBeenCalled(); }
  32. 32. 3. Spy class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } } ! public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $timer = $this->prophesize(LoginTimer::class); $system = new System($auth->reveal(), $timer->reveal()); $auth->authorize('everzet', '123')->willReturn(true); ! $system->login('everzet', '123'); ! $timer->recordLogin('everzet')->shouldHaveBeenCalled(); }
  33. 33. 4. Mock
  34. 34. 3. Spy class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } } ! public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $timer = $this->prophesize(LoginTimer::class); $system = new System($auth->reveal(), $timer->reveal()); $auth->authorize('everzet', '123')->willReturn(true); ! $system->login('everzet', '123'); ! $timer->recordLogin('everzet')->shouldHaveBeenCalled(); }
  35. 35. 4. Mock class System { // ... ! public function logIn($username, $password) { if ($this->authorizer->authorize($username, $password)) { $this->loginCount++; $this->lastLoginTimer->recordLogin($username); } } } ! public function testCountsSuccessfullyAuthorizedLogIns() { $auth = $this->prophesize(Authorizer::class); $timer = $this->prophesize(LoginTimer::class); $system = new System($auth->reveal(), $timer->reveal()); $auth->authorize('everzet', '123')->willReturn(true); ! $timer->recordLogin('everzet')->shouldBeCalled(); ! $system->login('everzet', '123'); ! $this->getProphet()->checkPredictions(); }
  36. 36. Back to the event dispatcher
  37. 37. Find the message public function testEventIsDispatchedDuringRegistration() { $dispatcher = new EventDispatcher(); $repository = new UserRepository(); $manager = new UserManager($repository, $dispatcher); ! $timesDispatched = 0; $dispatcher->addListener( 'userIsRegistered', function() use($timesDispatched) { $timesDispatched += 1; } ); ! $user = User::signup('ever.zet@gmail.com');  $manager->registerUser($user);   $this->assertSame(1, $timesDispatched); }
  38. 38. Communication over state public function testEventIsDispatchedDuringRegistration() { $repository = $this->prophesize(UserRepository::class); $dispatcher = $this->prophesize(EventDispatcher::class); $manager = new UserManager( $repository->reveal(), $dispatcher->reveal() );   $user = User::signup('ever.zet@gmail.com');  $manager->registerUser($user); ! $dispatcher->dispatch('userIsRegistered', Argument::any()) ->shouldHaveBeenCalled(); }
  39. 39. Exposed communication public function testEventIsDispatchedDuringRegistration() { $repository = $this->prophesize(UserRepository::class); $dispatcher = $this->prophesize(EventDispatcher::class); $manager = new UserManager( $repository->reveal(), $dispatcher->reveal() );   $user = User::signup('ever.zet@gmail.com');  $manager->registerUser($user); ! $dispatcher->dispatch('userIsRegistered', Argument::any()) ->shouldHaveBeenCalled(); }
  40. 40. Design?
  41. 41. ” – The Observer Effect “The act of observing will influence the phenomenon being observed.”
  42. 42. The 1st case: simple controller
  43. 43. Simple Symfony2 controller public function packagesListAction(Request $req, User $user) { $packages = $this->getDoctrine() ->getRepository('WebBundle:Package') ->getFilteredQueryBuilder(array('maintainer' => $user->getId())) ->orderBy('p.name') ->getQuery() ->execute(); ! return $this->render('WebBundle:User:packages.html.twig', [ 'packages' => $packages ]); }
  44. 44. “Simple” Symfony2 controller test public function testShowMaintainedPackages() { $request = new Request(); $user = new User('everzet'); $container = $this->prophesize(ContainerInterface::class); $doctrine = $this->prophesize(EntityManager::class); $repository = $this->prophesize(PackageRepository::class); $queryBuilder = $this->prophesize(QueryBuilder::class); $query = $this->prophesize(Query::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $templating = $this->prophesize(EngineInterface::class); $response = new Response('User packages'); ! $container->get('doctrine.orm')->willReturn($doctrine); $doctrine->getRepository('WebBundle:Package')->willReturn($repository); $repository->getFilteredQueryBuilder(['maintainer' => $user->getId()]) ->willReturn($queryBuilder); $queryBuilder->orderBy('p.name')->shouldBeCalled(); $queryBuilder->getQuery()->willReturn($query); $query->execute()->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController(); $controller->setContainer($container); $controllerResult = $controller->maintainsPackagesAction($request, $user); ! $this->assertEquals($response, $controllerResult); }
  45. 45. “Simple” Symfony2 controller test public function testShowMaintainedPackages() { $request = new Request(); $user = new User('everzet'); $container = $this->prophesize(ContainerInterface::class); $doctrine = $this->prophesize(EntityManager::class); $repository = $this->prophesize(PackageRepository::class); $queryBuilder = $this->prophesize(QueryBuilder::class); $query = $this->prophesize(Query::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $templating = $this->prophesize(EngineInterface::class); $response = new Response('User packages'); ! $container->get('doctrine.orm')->willReturn($doctrine); $doctrine->getRepository('WebBundle:Package')->willReturn($repository); $repository->getFilteredQueryBuilder(['maintainer' => $user->getId()]) ->willReturn($queryBuilder); $queryBuilder->orderBy('p.name')->shouldBeCalled(); $queryBuilder->getQuery()->willReturn($query); $query->execute()->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController(); $controller->setContainer($container); $controllerResult = $controller->maintainsPackagesAction($request, $user); ! $this->assertEquals($response, $controllerResult); }
  46. 46. “Simple” Symfony2 controller test public function testShowMaintainedPackages() { $request = new Request(); $user = new User('everzet'); $container = $this->prophesize(ContainerInterface::class); $doctrine = $this->prophesize(EntityManager::class); $repository = $this->prophesize(PackageRepository::class); $queryBuilder = $this->prophesize(QueryBuilder::class); $query = $this->prophesize(Query::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $templating = $this->prophesize(EngineInterface::class); $response = new Response('User packages'); ! $container->get('doctrine.orm')->willReturn($doctrine); $doctrine->getRepository('WebBundle:Package')->willReturn($repository); $repository->getFilteredQueryBuilder(['maintainer' => $user->getId()]) ->willReturn($queryBuilder); $queryBuilder->orderBy('p.name')->shouldBeCalled(); $queryBuilder->getQuery()->willReturn($query); $query->execute()->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController(); $controller->setContainer($container); $controllerResult = $controller->maintainsPackagesAction($request, $user); ! $this->assertEquals($response, $controllerResult); }
  47. 47. Single Responsibility Principle
  48. 48. Simpler Symfony2 controller simple test public function testShowMaintainedPackages() { $user = new User('everzet'); $repository = $this->prophesize(PackageRepository::class); $templating = $this->prophesize(EngineInterface::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $response = new Response('User packages'); ! $repository->getMaintainedPackagesOrderedByName($user)->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController($repository->reveal(), $templating->reveal()); $controllerResult = $controller->maintainsPackagesAction($user); ! $this->assertEquals($response, $controllerResult); }
  49. 49. Simpler Symfony2 controller simple test public function testShowMaintainedPackages() { $user = new User('everzet'); $repository = $this->prophesize(PackageRepository::class); $templating = $this->prophesize(EngineInterface::class); $packages = [new Package('Behat'), new Package('PhpSpec')]; $response = new Response('User packages'); ! $repository->getMaintainedPackagesOrderedByName($user)->willReturn($packages); $templating->renderResponse( 'WebBundle:User:packages.html.twig', ['packages' => $packages], null) ->willReturn($response); ! $controller = new UserController($repository->reveal(), $templating->reveal()); $controllerResult = $controller->maintainsPackagesAction($user); ! $this->assertEquals($response, $controllerResult); }
  50. 50. Simpler Symfony2 controller public function maintainsPackagesAction(User $user) { $packages = $this->repo->getMaintainedPackagesOrderedByName($user); ! return $this->tpl->renderResponse('WebBundle:User:packages.html.twig', [ 'packages' => $packages ]); }
  51. 51. The 2nd case: basket checkout
  52. 52. Basket checkout class Basket { // ... ! public function checkout(OrderProcessor $processor) { $totalPrice = new Price::free(); foreach ($this->getItems() as $item) { $totalPrice = $totalPrice->add($item->getPrice()); $processor->addItem($item); } ! $payment = new CashPayment::fromPrice($totalPrice); $processor->setPayment($payment);   $processor->pay(); } }
  53. 53. Basket checkout test public function testCheckout() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::which('getPrice', 15))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }
  54. 54. Basket checkout test two payments public function testCheckoutWithCash() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = false); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CashPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }   public function testCheckoutWithCreditCard() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = true); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CreditPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }
  55. 55. Basket checkout test two payments public function testCheckoutWithCash() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = false); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CashPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }   public function testCheckoutWithCreditCard() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = true); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CreditPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }
  56. 56. Basket checkout duplication in test public function testCheckoutWithCash() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = false); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CashPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }   public function testCheckoutWithCreditCard() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class);   $basket = new Basket($items, $credit = true); $basket->checkout($processor->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment(Argument::allOf( Argument::type(CreditPayment::class), Argument::which('getPrice', 15) ))->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }
  57. 57. Open Closed Principle
  58. 58. Basket checkout test simplification public function testCheckoutWithPaymentMethod() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class); $paymentMethod = $this->prophesize(PaymentMethod::class); $payment = $this->prophesize(Payment::class);   $paymentMethod->acceptPayment(Price::fromInt(15))->willReturn($payment);   $basket = new Basket($items); $basket->checkout($processor->reveal(), $paymentMethod->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment($payment)->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }
  59. 59. Basket checkout test simplification public function testCheckoutWithPaymentMethod() { $items = [new Item(Price::fromInt(10), Price::fromInt(5)]; $processor = $this->prophesize(OrderProcessor::class); $paymentMethod = $this->prophesize(PaymentMethod::class); $payment = $this->prophesize(Payment::class);   $paymentMethod->acceptPayment(Price::fromInt(15))->willReturn($payment);   $basket = new Basket($items); $basket->checkout($processor->reveal(), $paymentMethod->reveal());   $processor->addItem($items[0])->shouldHaveBeenCalled(); $processor->addItem($items[1])->shouldHaveBeenCalled(); $processor->setPayment($payment)->shouldHaveBeenCalled(); $processor->pay()->shouldHaveBeenCalled(); }
  60. 60. Final basket checkout class Basket { // ...   public function checkout(OrderProcessor $processor, PaymentMethod $method) { $totalPrice = new Price::free(); foreach ($this->getItems() as $item) { $totalPrice = $totalPrice->add($item->getPrice()); $processor->addItem($item); }   $payment = $method->acceptPayment($totalPrice); $processor->setPayment($payment);   $processor->pay(); } }
  61. 61. Final basket checkout class Basket { // ...   public function checkout(OrderProcessor $processor, PaymentMethod $method) { $totalPrice = new Price::free(); foreach ($this->getItems() as $item) { $totalPrice = $totalPrice->add($item->getPrice()); $processor->addItem($item); }   $payment = $method->acceptPayment($totalPrice); $processor->setPayment($payment);   $processor->pay(); } }
  62. 62. The 3rd case: browser emulation
  63. 63. Browser class Browser { public function __construct(BrowserDriver $driver) { $this->driver = $driver; }   public function goto($url) { $this->driver->boot(); $this->driver->visit($url); } }
  64. 64. Browser drivers interface BrowserDriver { public function boot(); public function visit($url); } ! interface HeadlessBrowserDriver extends BrowserDriver {} ! class SeleniumDriver implements BrowserDriver { public function boot() { $this->selenium->startBrowser($this->browser); } ! public function visit($url) { $this->selenium->visitUrl($url); } } ! class GuzzleDriver implements HeadlessBrowserDriver { public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }
  65. 65. Headless driver test public function testVisitingProvidedUrl() { $url = 'http://en.wikipedia.org'; $driver = $this->prophesize(HeadlessBrowserDriver::class); ! $driver->visit($url)->shouldBeCalled(); ! $browser = new Browser($driver->reveal()); $browser->goto($url); ! $this->getProphecy()->checkPredictions(); }
  66. 66. Failing headless driver test public function testVisitingProvidedUrl() { $url = 'http://en.wikipedia.org'; $driver = $this->prophesize(HeadlessBrowserDriver::class); ! $driver->visit($url)->shouldBeCalled(); ! $browser = new Browser($driver->reveal()); $browser->goto($url); ! $this->getProphecy()->checkPredictions(); }
  67. 67. Refused Bequest
  68. 68. Headless driver implementation class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }
  69. 69. Headless driver simple behaviour class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() {} ! public function visit($url) { $this->guzzle->openUrl($url); } }
  70. 70. Headless driver that knows about booting class GuzzleDriver implements HeadlessBrowserDriver { ! public function boot() { $this->allowDoActions = true; }   public function visit($url) { if ($this->allowDoActions) $this->guzzle->openUrl($url); } }
  71. 71. Liskov Substitution Principle
  72. 72. Adapter layer between BrowserDriver and HeadlessBrowserDriver interface HeadlessBrowserDriver { public function visit($url); } ! class GuzzleDriver implements HeadlessBrowserDriver { public function visit($url) { $this->guzzle->openUrl($url); } } ! final class HeadlessBrowserAdapter implements BrowserDriver { private $headlessDriver, $allowDoAction = false; ! public function __construct(HeadlessBrowserDriver $headlessDriver) { $this->headlessDriver = $headlessDriver; } ! public function boot() { $this->allowDoActions = true; } ! public function visit($url) { if ($this->allowDoActions) $this->headlessDriver->visit($url); } }
  73. 73. Dirty adapter layer between BrowserDriver and HeadlessBrowserDriver interface HeadlessBrowserDriver { public function visit($url); } ! class GuzzleDriver implements HeadlessBrowserDriver { public function visit($url) { $this->guzzle->openUrl($url); } } ! final class HeadlessBrowserAdapter implements BrowserDriver { private $headlessDriver, $allowDoAction = false; ! public function __construct(HeadlessBrowserDriver $headlessDriver) { $this->headlessDriver = $headlessDriver; } ! public function boot() { $this->allowDoActions = true; } ! public function visit($url) { if ($this->allowDoActions) $this->headlessDriver->visit($url); } }
  74. 74. Single adapter layer between BrowserDriver and HeadlessBrowserDriver interface HeadlessBrowserDriver { public function visit($url); } ! class GuzzleDriver implements HeadlessBrowserDriver { public function visit($url) { $this->guzzle->openUrl($url); } } ! final class HeadlessBrowserAdapter implements BrowserDriver { private $headlessDriver, $allowDoAction = false; ! public function __construct(HeadlessBrowserDriver $headlessDriver) { $this->headlessDriver = $headlessDriver; } ! public function boot() { $this->allowDoActions = true; } ! public function visit($url) { if ($this->allowDoActions) $this->headlessDriver->visit($url); } }
  75. 75. The 4th case: ATM screen
  76. 76. ATM messenger interface interface Messenger { public function askForCard(); public function askForPin(); public function askForAccount(); public function askForAmount(); public function tellNoMoney(); public function tellMachineEmpty(); }
  77. 77. City ATM login test public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(Messenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }
  78. 78. City ATM login test public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(Messenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }
  79. 79. Interface Segregation Principle
  80. 80. City ATM login test public function testAtmAsksForCardAndPinDuringLogin() { $messenger = $this->prophesize(LoginMessenger::class); ! $messenger->askForCard()->shouldBeCalled(); $messenger->askForPin()->shouldBeCalled(); ! $atm = new CityAtm($messenger->reveal()); $atm->login(); ! $this->getProphet()->checkPredictions(); }
  81. 81. ATM messenger interface(s) interface LoginMessenger { public function askForCard(); public function askForPin(); } ! interface InputMessenger { public function askForAccount(); public function askForAmount(); } ! interface WithdrawalMessenger { public function tellNoMoney(); public function tellMachineEmpty(); } ! interface Messenger extends LoginMessenger, InputMessenger, WithdrawalMessenger
  82. 82. The 5th case: entity repository
  83. 83. Doctrine entity repository class JobRepository extends EntityRepository { public function findJobByName($name) { return $this->findOneBy(['name' => $name]); } }
  84. 84. Doctrine entity repository test public function testFindingJobsByName() { $em = $this->prophesize('EntityManager'); $cmd = $this->prophesize('ClassMetadata'); $uow = $this->prophesize('UnitOfWork'); $ep = $this->prophesize('EntityPersister'); $job = Job::fromName('engineer'); ! $em->getUnitOfWork()->willReturn($uow); $uow->getEntityPersister(Argument::any())->willReturn($ep); $ep->load(['name' => 'engineer'], null, null, [], null, 1, null) ->willReturn($job); ! $repo = new JobRepository($em->reveal(), $cmd->reveal()); $actualJob = $repo->findJobByName('engineer'); ! $this->assertSame($job, $actualJob); }
  85. 85. Doctrine entity repository test public function testFindingJobsByName() { $em = $this->prophesize('EntityManager'); $cmd = $this->prophesize('ClassMetadata'); $uow = $this->prophesize('UnitOfWork'); $ep = $this->prophesize('EntityPersister'); $job = Job::fromName('engineer'); ! $em->getUnitOfWork()->willReturn($uow); $uow->getEntityPersister(Argument::any())->willReturn($ep); $ep->load(['name' => 'engineer'], null, null, [], null, 1, null) ->willReturn($job); ! $repo = new JobRepository($em->reveal(), $cmd->reveal()); $actualJob = $repo->findJobByName('engineer'); ! $this->assertSame($job, $actualJob); }
  86. 86. Do not mock things you do not own
  87. 87. Doctrine entity repository test public function testFindingJobsByName() { $em = $this->prophesize('EntityManager'); $cmd = $this->prophesize('ClassMetadata'); $uow = $this->prophesize('UnitOfWork'); $ep = $this->prophesize('EntityPersister'); $job = Job::fromName('engineer'); ! $em->getUnitOfWork()->willReturn($uow); $uow->getEntityPersister(Argument::any())->willReturn($ep); $ep->load(['name' => 'engineer'], null, null, [], null, 1, null) ->willReturn($job); ! $repo = new JobRepository($em->reveal(), $cmd->reveal()); $actualJob = $repo->findJobByName('engineer'); ! $this->assertSame($job, $actualJob); }
  88. 88. Dependency Inversion Principle
  89. 89. Job repository & Doctrine implementation of it interface  JobRepository  {          public  function  findJobByName($name);   }   ! class  DoctrineJobRepository  extends  EntityRepository                                                          implements  JobRepository  {   !        public  function  findJobByName($name)  {                  return  $this-­‐>findOneBy(['name'  =>  $name]);          }   }
  90. 90. Job repository & Doctrine implementation of it interface  JobRepository  {          public  function  findJobByName($name);   }   ! class  DoctrineJobRepository  extends  EntityRepository                                                          implements  JobRepository  {   !        public  function  findJobByName($name)  {                  return  $this-­‐>findOneBy(['name'  =>  $name]);          }   }
  91. 91. Job repository & Doctrine implementation of it interface  JobRepository  {          public  function  findJobByName($name);   }   ! class  DoctrineJobRepository  extends  EntityRepository                                                          implements  JobRepository  {   !        public  function  findJobByName($name)  {                  return  $this-­‐>findOneBy(['name'  =>  $name]);          }   }
  92. 92. Recap:
  93. 93. Recap: 1. State-focused TDD is not the only way to TDD
  94. 94. Recap: 1. State-focused TDD is not the only way to TDD 2. Messaging is far more important concept of OOP than the state
  95. 95. Recap: 1. State-focused TDD is not the only way to TDD 2. Messaging is far more important concept of OOP than the state 3. By focusing on messaging, you expose messaging problems
  96. 96. Recap: 1. State-focused TDD is not the only way to TDD 2. Messaging is far more important concept of OOP than the state 3. By focusing on messaging, you expose messaging problems 4. By exposing messaging problems, you could discover most of the SOLID principles violation before they happen
  97. 97. Recap: 1. State-focused TDD is not the only way to TDD 2. Messaging is far more important concept of OOP than the state 3. By focusing on messaging, you expose messaging problems 4. By exposing messaging problems, you could discover most of the SOLID principles violation before they happen 5. Prophecy is awesome
  98. 98. 6. Messages define objects behaviour
  99. 99. Thank you!

×