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!