PHPSpec - the only Design Tool you need - 4Developers

Preview:

DESCRIPTION

Slides from my talk at 4Developers conference in Warsaw

Citation preview

PHPSpecthe only Design Tool you need

flickr.com/mobilestreetlife/4179063482/

Kacper Gunia @cakper Software Engineer @SensioLabsUK

Symfony Certified Developer

PHPers Silesia @PHPersPL

!

‘Is my code well designed?’

’That’s not the way

I would have done it…’

It’s hard tochange!

We're afraid to change it…

We cannotreuse it!

So what

Design is about?

‘The key in making great and growable systems is much more to design how its

modules communicate rather than what their internal

properties and behaviors should be.’ Alan Kay

Design is about

Messaging

$orders  =  $orderRepository      -­‐>getEntityManager()        -­‐>createOrderQuery($customer)        -­‐>execute();

$orders  =  $orderRepository      -­‐>findBy($customer);  !

We have

to refactor! :)

We need two weeks to refactor! :)

We need two sprints to refactor! :|

We need two months

to refactor! :/

Refactoringis the process of restructuring existing code without changing its external behavior

4 Rules of Simple Design1. Passes its tests 2. Minimizes duplication 3. Maximizes clarity 4. Has fewer elements

…so we need to write

Tests!

how to write

Tests?

Tests Driven Development

Red

GreenRefactor

Red

GreenRefactor

But!

How to test something that doesn’t exist?

flickr.com/ucumari/580865728/

Test in TDD means

specification

Specification describes behavior

Behavior Driven Development

BDD improvesNaming Conventions Tools

Story BDD vs

Spec BDD

Story BDDdescription of business-targeted application behavior

Spec BDDspecification for low-level implementation

http://phpspec.net/

Spec BDD tool created by

@_md & @everzet

Bundled with mocking library

Prophecy

composer  create-­‐project                                      cakper/phpspec-­‐standard                    project-­‐name

But!

Isn’t it a tool just like PHPUnit?

PHPUnit is a

Testing Tool

PHPSpec is the

Design Tool

class  CustomerRepositoryTest  extends  \PHPUnit_Framework_TestCase  {          function  testClassExists()          {                  $customerRepository  =  new  \CustomerRepository;  !

               $customer  =  $customerRepository-­‐>findById(5);                  $this-­‐>assertInstanceOf('\Customer',  $customer);          }  }

class  CustomerRepositorySpec  extends  ObjectBehavior  {          function  it_is_initializable()          {                  $this-­‐>shouldHaveType('CustomerRepository');          }  }

Naming

TestCase !

Specification

Test !

Example

Assertion !

Expectation

OK, so how to specify a method?

What method can do?return a value modify state delegate throw an exception

Command-Query Separation

Commandchange the state of a system but do not return a value

Queryreturn a result and do not change the state of the system (free of side effects)

Never both!

class  CustomerRepositorySpec  extends  ObjectBehavior  {          function  it_loads_user_preferences()          {                  $customer  =  $this-­‐>findById(5);  !

               $customer-­‐>shouldBeAnInstanceOf('\Customer');          }  }

Matchers

TypeshouldBeAnInstanceOf(*) shouldReturnAnInstanceOf(*) shouldHaveType(*)

$customer-­‐>shouldBeAnInstanceOf('\Customer');

Identity ===shouldReturn(*) shouldBe(*) shouldEqual(*) shouldBeEqualTo(*)

$this-­‐>findById(-­‐1)-­‐>shouldReturn(null);

Comparison ==shouldBeLike(*)

$this-­‐>getAmount()-­‐>shouldBeLike(5);

Throwthrow(*)->during*()

$this-­‐>shouldThrow(‘\InvalidArgumentException’)          -­‐>duringFindByCustomer(null);

Object StateshouldHave*()

$car-­‐>hasEngine();  !

$this-­‐>shouldHaveEngine();

ScalarshouldBeString() shouldBeArray()

CountshouldHaveCount(*)

Or write your own

Inline Matcher

function  it_should_have_poland_as_avialable_country()  {          $this-­‐>getCountryCodes()-­‐>shouldHaveValue('PL');  }  !

public  function  getMatchers()  {          return  [                  'haveValue'  =>  function  ($subject,  $value)  {                                  return  in_array($value,  $subject);                          }          ];  }

But!

Design is about

Messaging!

And (so far) there is no messaging…

London School

Mockist TDDonly tested object is real

Test Doubles

Dummytested code requires parameter but doesn’t need to use it

function  let(EntityManager  $entityManager)  {          $this-­‐>beConstructedWith($entityManager);  }  !

function  it_returns_customer_by_id()  {          $customer  =  $this-­‐>findById(5);  !

       $customer-­‐>shouldBeAnInstanceOf('\Customer');          }  }

function  let(EntityManager  $entityManager)  {          $this-­‐>beConstructedWith($entityManager);  }  !

function  it_returns_customer_by_id()  {          $customer  =  $this-­‐>findById(5);  !

       $customer-­‐>shouldBeAnInstanceOf('\Customer');          }  }

Stubprovides "indirect input" to the tested code

function  it_bolds_the_output(Stream  $stream)  {          $stream-­‐>getOutput()                        -­‐>willReturn('4  Developers');  !

       $this-­‐>bold($stream)                    -­‐>shouldReturn('<b>4  Developers</b>’);  }

function  it_bolds_the_output(Stream  $stream)  {          $stream-­‐>getOutput()                        -­‐>willReturn('4  Developers');  !

       $this-­‐>bold($stream)                    -­‐>shouldReturn('<b>4  Developers</b>’);  }

Mocksverifies "indirect output” of the tested code

function  let(Logger  $logger)  {          $this-­‐>beConstructedWith($logger);  }  !

function  it_returns_customer_by_id(Logger  $logger)  {          $logger-­‐>debug('DB  queried')                        -­‐>shouldBeCalled();  !

       $this-­‐>findById(5);  }

function  let(Logger  $logger)  {          $this-­‐>beConstructedWith($logger);  }  !

function  it_returns_customer_by_id(Logger  $logger)  {          $logger-­‐>debug('DB  queried')                        -­‐>shouldBeCalled();  !

       $this-­‐>findById(5);  }

Spyverifies "indirect output” by asserting the expectations afterwards

function  let(Logger  $logger)  {          $this-­‐>beConstructedWith($logger);  }  !

function  it_returns_customer_by_id(Logger  $logger)  {          $this-­‐>findById(5);  !

       $logger-­‐>debug('DB  queried')                        -­‐>shouldHaveBeenCalled();  }

function  let(Logger  $logger)  {          $this-­‐>beConstructedWith($logger);  }  !

function  it_returns_customer_by_id(Logger  $logger)  {          $this-­‐>findById(5);  !

       $logger-­‐>debug('DB  queried')                        -­‐>shouldHaveBeenCalled();  }

(a bit more) complex example

function  let(SecurityContext  $securityContext)  {          $this-­‐>beConstructedWith($securityContext);    }  !

function  it_loads_user_preferences(          GetResponseEvent  $event,  SecurityContext  $securityContext,            TokenInterface  $token,  User  $user)    {          $securityContext-­‐>getToken()-­‐>willReturn($token);          $token-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type('Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);    }

function  let(SecurityContext  $securityContext)  {          $this-­‐>beConstructedWith($securityContext);    }  !

function  it_loads_user_preferences(          GetResponseEvent  $event,  SecurityContext  $securityContext,            TokenInterface  $token,  User  $user)    {          $securityContext-­‐>getToken()-­‐>willReturn($token);          $token-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type('Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);    }

function  let(SecurityContext  $securityContext)  {          $this-­‐>beConstructedWith($securityContext);    }  !

function  it_loads_user_preferences(          GetResponseEvent  $event,  SecurityContext  $securityContext,            TokenInterface  $token,  User  $user)    {          $securityContext-­‐>getToken()-­‐>willReturn($token);          $token-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type('Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);    }

function  let(SecurityContext  $securityContext)  {          $this-­‐>beConstructedWith($securityContext);    }  !

function  it_loads_user_preferences(          GetResponseEvent  $event,  SecurityContext  $securityContext,            TokenInterface  $token,  User  $user)    {          $securityContext-­‐>getToken()-­‐>willReturn($token);          $token-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type('Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);    }

function  let(SecurityContext  $securityContext)  {          $this-­‐>beConstructedWith($securityContext);    }  !

function  it_loads_user_preferences(          GetResponseEvent  $event,  SecurityContext  $securityContext,            TokenInterface  $token,  User  $user)    {          $securityContext-­‐>getToken()-­‐>willReturn($token);          $token-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type('Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);    }

But mocking becomes painful…

And it smells…

Law of Demeterunit should only talk to its friends; don't talk to strangers

It’s time to refactor! :)

function  it_loads_user_preferences(          GetResponseEvent  $event,            SecurityContext  $securityContext,            TokenInterface  $token,  User  $user)    {          $securityContext-­‐>getToken()-­‐>willReturn($token);          $token-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type('Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);    }

function  it_returns_user_from_token(          SecurityContext  $securityContext,            TokenInterface  $token,  User  $user)  {          $securityContext-­‐>getToken()-­‐>willReturn($token);          $token-­‐>getUser()-­‐>willReturn($user);  !

       $this-­‐>getUser()-­‐>shouldRetun($user);  }  !

public  function  __construct(SecurityContext  $securityContext){          $this-­‐>securityContext  =  $securityContext;  }  !

public  function  getUser()  {          $token  =  $this-­‐>securityContext-­‐>getToken();          if  ($token  instanceof  TokenInterface)  {                  return  $token-­‐>getUser();          }  !

       return  null;  }

function  it_loads_user_preferences(          GetResponseEvent  $event,            SecurityContext  $securityContext,            TokenInterface  $token,  User  $user)    {          $securityContext-­‐>getToken()-­‐>willReturn($token);          $token-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type('Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);    }

function  it_loads_user_preferences(          GetResponseEvent  $event,                                  DomainSecurityContext  $securityContext,          User  $user)  {          $securityContext-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type(‘Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);  }  

Composition over Inheritance

separation of concerns small, well focused objects composition is simpler to test

public  function  __construct(SecurityContext  $securityContext){          $this-­‐>securityContext  =  $securityContext;  }  !

public  function  getUser()  {          $token  =  $this-­‐>securityContext-­‐>getToken();          if  ($token  instanceof  TokenInterface)  {                  return  $token-­‐>getUser();          }  !

       return  null;  }

We can still improve

function  it_loads_user_preferences(          GetResponseEvent  $event,                                  DomainSecurityContext  $securityContext,          User  $user)  {          $securityContext-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type(‘Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);  }  

function  it_loads_user_preferences(          GetResponseEvent  $event,                                  DomainSecurityContextInterface  $securityContext,          User  $user)  {          $securityContext-­‐>getUser()-­‐>willReturn($user);  !

       $user-­‐>setPreferences(Argument::type(‘Preferences'))                    -­‐>shouldBeCalled();  !

       $this-­‐>handle($event);  }  

Dependency Inversion Principle

high-level modules should not depend on low-level modules; both should depend on abstractions

DIP states:

DIP states:abstractions should not depend upon details; details should depend upon abstractions

Isn’t it overhead?

What are benefits of using PHPSpec?

TDD-cycle oriented tool

ease Mocking

focused on Messaging

encourage injecting right Collaborators

and following Demeter Low

enables Refactoring

and gives you Regression Safety

and it’s trendy ;)

Is PHPSpec the only design tool

we need?

s

So it helps ;)

Kacper Gunia Software Engineer

Symfony Certified Developer

PHPers Silesia

Thanks!