So in response to the recent series Pete has started here I cloned his repo and was playing with what I have been seeing around.
Previous episodes:
I will explain it here step by step:
1. It was suggested that instead of BlogManager for the example shown we should use BlogRepository since it was more DDD friendly and more conveying the model we had so far. However we did this, I believe the correct model should not be even BlogRepository in the sense of Doctrine but in the sense of Model Driven Development which calls for this layer to be completely implementation agnostic.
2. Notice we have moved everything to annotations instead of typehinting. This specs don’t need typehinting in the sense of final classes but they do need readability. It seems to me the annotations are exactly more conveying than the typehinting which clog the signatures. I also don’t like `shoulds` on the method names.
<?php namespace spec\Peterjmit\BlogBundle\Controller; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Symfony\Component\HttpFoundation\Response; class BlogControllerSpec extends ObjectBehavior { /** * @param \Doctrine\ORM\EntityRepository $blogRepository * @param \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface $templating */ function let($blogRepository, $templating) { $this->beConstructedWith($blogRepository, $templating); } function it_is_initializable() { $this->shouldHaveType('Peterjmit\BlogBundle\Controller\BlogController'); } /** * @param \Doctrine\ORM\EntityRepository $blogRepository * @param \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface $templating */ function it_responds_to_index_action($blogRepository, $templating) { $response = new Response(); $blogRepository->findAll()->willReturn(['An array', 'of blog', 'posts!']); $templating ->renderResponse( 'PeterjmitBlogBundle:Blog:index.html.twig', ['posts' => ['An array', 'of blog', 'posts!']] ) ->willReturn($response) ; $response = $this->indexAction(); $response->shouldHaveType('Symfony\Component\HttpFoundation\Response'); } /** * @param \Doctrine\ORM\EntityRepository $blogRepository * @param \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface $templating */ function it_shows_a_single_blog_post($blogRepository, $templating) { $response = new Response(); $blogRepository->find(1)->willReturn('A blog post'); $templating ->renderResponse( 'PeterjmitBlogBundle:Blog:show.html.twig', Argument::withEntry('post', 'A blog post') ) ->willReturn($response) ; $this->showAction(1)->shouldReturn($response); } /** * @param \Doctrine\ORM\EntityRepository $blogRepository */ function it_throws_an_exception_if_a_blog_post_doesnt_exist($blogRepository) { $blogRepository->find(999)->willReturn(null); $this ->shouldThrow('Symfony\Component\HttpKernel\Exception\NotFoundHttpException') ->duringShowAction(999) ; } } |
3. Going CAS (controllers as services) is a given already and it is natural in specking with model driven development in mind. In this case we define a controller service to which we inject the blog repository service and the templating engine service. Notice the repository blog service gets injected the entity manager, not a beautiful thing since this is already an implementation detail, however it tells us that we need to pull metadata mapping information from some model, so that is the right hint. However it would have been nicer rather to have a gateway layer. We of course define this metadata in the constructor of the repository to keep it for now in one place as shown below:
services: peterjmit_blog.controller.blog: class: Peterjmit\BlogBundle\Controller\BlogController arguments: - @peterjmit_blog.repository.blog - @templating peterjmit_blog.repository.blog: class: Peterjmit\BlogBundle\Repository\BlogRepository arguments: - @entity_manager |
This is how the repository service looks like:
<?php namespace Peterjmit\BlogBundle\Repository; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Mapping\ClassMetadata; class BlogRepository extends EntityRepository { public function __construct($em) { $entityName = 'Peterjmit\BlogBundle\Model\Blog'; $class = new ClassMetadata($entityName); parent::__construct($em, $class); } } |
4. We don’t need to pass to the spec methods a Response object. It is not mocking that that object does but stubbing. There is a difference I think. And I rather stub it and create the object by hand when it is needed as shown in the first piece of code above.
Here is the diff of the commits I PR to the original repo, great job so far!
Credits to Pete for making things simple and prompting me to respond avidly!
About applied phpspec on crud, there is ResourceBundle from Sylius who does a great job. I use it and explain some things related to Symfony2 Ecommerce on the Lean Book http://pilotci.com.
Let’s keep specking!