Dutch PHP Conference - PHPSpec 2 - The only Design Tool you need

140
PHPSpec the only Design Tool you need flickr.com/dad/5528226004

description

Slides from my talk at Dutch PHP Conference in Amsterdam

Transcript of Dutch PHP Conference - PHPSpec 2 - The only Design Tool you need

  • PHPSpecthe only Design Tool you need flickr.com/dad/5528226004

Kacper Gunia @cakper Software Engineer @SensioLabsUK Symfony Certied Developer PHPers Silesia @PHPersPL ! Is my code well designed? Thats not the way I would have done it Its hard to change! Its hard to change!Rigidity We're afraid to change it We're to change itFragility We cannot reuse it! We cannot reuse it!Immobility What is Design 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! :/ Refactoring is the process of restructuring existing code without changing its external behavior 4 Rules of Simple Design 1. 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 doesnt exist? flickr.com/ucumari/580865728/ Test in TDD means specication Specication describes behavior Behavior Driven Development BDD improves Naming Conventions Tools Story BDD vs Spec BDD Story BDD description of business-targeted application behavior Spec BDD specication for low-level implementation http://phpspec.net/ Spec BDD tool created by @_md & @everzet Bundled with mocking library Prophecy But! Isnt it a tool just like PHPUnit? PHPUnit is a Testing Tool PHPSpec is the Design Tool classCustomerRepositoryTestextends PHPUnit_Framework_TestCase { functiontestFindCustomerById() { $customerRepository=newCustomerRepository; ! $customer=$customerRepository->findById(5); $this->assertInstanceOf('Customer',$customer); } } classCustomerRepositorySpecextendsObjectBehavior { functionit_is_initializable() { $this->shouldHaveType('CustomerRepository'); } } Naming TestCase ! Specication 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 Command change the state of a system but do not return a value Query return a result and do not change the state of the system (free of side eects) Never both! classCustomerRepositorySpecextendsObjectBehavior { functionit_loads_user_preferences() { $customer=$this->findById(5); ! $customer->shouldBeAnInstanceOf('Customer'); } } Matchers Type shouldBeAnInstanceOf(*) shouldReturnAnInstanceOf(*) shouldHaveType(*) $customer->shouldBeAnInstanceOf('Customer'); Identity === shouldReturn(*) shouldBe(*) shouldEqual(*) shouldBeEqualTo(*) $this->findById(-1)->shouldReturn(null); Comparison == shouldBeLike(*) $this->getAmount()->shouldBeLike(5); Throw throw(*)->during*() $this->shouldThrow(InvalidArgumentException) ->duringFindByCustomer(null); Object State shouldHave*() $car->hasEngine(); ! $this->shouldHaveEngine(); Scalar shouldBeString() shouldBeArray() Count shouldHaveCount(*) Or write your own Inline Matcher functionit_should_have_holland_as_avialable_country() { $this->getCountryCodes()->shouldHaveValue('NL'); } ! publicfunctiongetMatchers() { return[ 'haveValue'=>function($subject,$value){ returnin_array($value,$subject); } ]; } But! Design is about Messaging! And (so far) there is no messaging London School Mockist TDD only tested object is real Test Doubles Dummy tested code requires parameter but doesnt need to use it functionlet(EntityManager$entityManager) { $this->beConstructedWith($entityManager); } ! functionit_returns_customer_by_id() { $customer=$this->findById(5); ! $customer->shouldBeAnInstanceOf('Customer'); } } functionlet(EntityManager$entityManager) { $this->beConstructedWith($entityManager); } ! functionit_returns_customer_by_id() { $customer=$this->findById(5); ! $customer->shouldBeAnInstanceOf('Customer'); } } Stub provides "indirect input" to the tested code functionit_bolds_the_output(Stream$stream) { $stream->getOutput() ->willReturn('DPC'); ! $this->bold($stream) ->shouldReturn('DPC); } functionit_bolds_the_output(Stream$stream) { $stream->getOutput() ->willReturn('DPC'); ! $this->bold($stream) ->shouldReturn('DPC); } Mocks veries "indirect output of the tested code functionlet(Logger$logger) { $this->beConstructedWith($logger); } ! functionit_returns_customer_by_id(Logger$logger) { $logger->debug('DBqueried') ->shouldBeCalled(); ! $this->findById(5); } functionlet(Logger$logger) { $this->beConstructedWith($logger); } ! functionit_returns_customer_by_id(Logger$logger) { $logger->debug('DBqueried') ->shouldBeCalled(); ! $this->findById(5); } Spy veries "indirect output by asserting the expectations afterwards functionlet(Logger$logger) { $this->beConstructedWith($logger); } ! functionit_returns_customer_by_id(Logger$logger) { $this->findById(5); ! $logger->debug('DBqueried') ->shouldHaveBeenCalled(); } functionlet(Logger$logger) { $this->beConstructedWith($logger); } ! functionit_returns_customer_by_id(Logger$logger) { $this->findById(5); ! $logger->debug('DBqueried') ->shouldHaveBeenCalled(); } (a bit more) complex example functionlet(SecurityContext$securityContext){ $this->beConstructedWith($securityContext); } ! functionit_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); } functionlet(SecurityContext$securityContext){ $this->beConstructedWith($securityContext); } ! functionit_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); } functionlet(SecurityContext$securityContext){ $this->beConstructedWith($securityContext); } ! functionit_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); } functionlet(SecurityContext$securityContext){ $this->beConstructedWith($securityContext); } ! functionit_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); } functionlet(SecurityContext$securityContext){ $this->beConstructedWith($securityContext); } ! functionit_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 Demeter unit should only talk to its friends; don't talk to strangers Its time to refactor! :) functionit_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); } functionit_returns_user_from_token( SecurityContext$securityContext, TokenInterface$token,User$user) { $securityContext->getToken()->willReturn($token); $token->getUser()->willReturn($user); ! $this->getUser()->shouldReturn($user); } ! publicfunction__construct(SecurityContext$securityContext){ $this->securityContext=$securityContext; } ! publicfunctiongetUser() { $token=$this->securityContext->getToken(); if($tokeninstanceofTokenInterface){ return$token->getUser(); } ! returnnull; } functionit_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); } functionit_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 publicfunction__construct(SecurityContext$securityContext){ $this->securityContext=$securityContext; } ! publicfunctiongetUser() { $token=$this->securityContext->getToken(); if($tokeninstanceofTokenInterface){ return$token->getUser(); } ! returnnull; } We can still improve functionit_loads_user_preferences( GetResponseEvent$event, DomainSecurityContext$securityContext, User$user) { $securityContext->getUser()->willReturn($user); ! $user->setPreferences(Argument::type(Preferences')) ->shouldBeCalled(); ! $this->handle($event); } functionit_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 Isnt it overhead? What are benets of using PHPSpec? TDD-cycle oriented tool ease Mocking focused on Messaging encourages injecting right Collaborators and following Law of Demeter enables Refactoring and gives you Regression Safety and its trendy ;) Is PHPSpec the only design tool we need? s So it helps ;) Kacper Gunia Software Engineer Symfony Certied Developer PHPers Silesia Thanks! joind.in/10864