Search code examples
unit-testingmockingbddfunctional-testing

How to correctly test repository with xSpec tool?


I learn TDD and I've started to use xSpec tool (language does not matter, but it's phpspec2 in my case). I write my first specification:

<?php

namespace spec\Mo\SpeechBundle\Controller;

use Doctrine\Common\Collections\Collection;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
use Mo\SpeechBundle\Repository\IdeaRepository;
use Mo\SpeechBundle\Repository\SpeechRepository;
use Mo\SpeechBundle\Entity\Idea;
use Mo\SpeechBundle\Entity\Speech;

class SpeechControllerSpec extends ObjectBehavior
{
    function let(SpeechRepository $speechRepository, IdeaRepository $ideaRepository, EngineInterface $templating)
    {
        $this->beConstructedWith($speechRepository, $ideaRepository, $templating);
    }

    function it_is_initializable()
    {
        $this->shouldHaveType('Mo\SpeechBundle\Controller\SpeechController');
    }

    function it_responds_to_show_action(EngineInterface $templating, Speech $speech, Response $response)
    {
        $templating
            ->renderResponse('MoSpeechBundle:Speech:show.html.twig', ['speech' => $speech])
            ->willReturn($response)
        ;

        $this->showAction($speech)->shouldBeAnInstanceOf('Symfony\Component\HttpFoundation\Response');
    }

    function it_responds_to_list_action(
        SpeechRepository $speechRepository,
        IdeaRepository $ideaRepository,
        EngineInterface $templating,
        Response $response
    )
    {
        $speeches = [new Speech()];
        $ideas = [new Idea()];

        $speechRepository->findAll()->willReturn($speeches);
        $ideaRepository->findAll()->willReturn($ideas);

        $templating
            ->renderResponse('MoSpeechBundle:Speech:list.html.twig', compact('speeches', 'ideas'))
            ->willReturn($response)
        ;

        $this->listAction()->shouldBeAnInstanceOf('Symfony\Component\HttpFoundation\Response');
    }

    function it_responds_list_by_idea_action(
        Idea $idea,
        SpeechRepository $speechRepository,
        IdeaRepository $ideaRepository,
        EngineInterface $templating,
        Response $response
    )
    {
        $speeches = [new Speech()];
        $ideas = [new Idea()];

        $speechRepository->findByIdea($idea)->willReturn($speeches);
        $ideaRepository->findAll()->willReturn($ideas);

        $templating
            ->renderResponse('MoSpeechBundle:Speech:list.html.twig', compact('speeches', 'idea', 'ideas'))
            ->willReturn($response)
        ;

        $this->listByIdeaAction($idea)->shouldBeAnInstanceOf('Symfony\Component\HttpFoundation\Response');
    }
}

For controller:

<?php

namespace Mo\SpeechBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Mo\SpeechBundle\Repository\IdeaRepository;
use Mo\SpeechBundle\Repository\SpeechRepository;
use Mo\SpeechBundle\Entity\Idea;
use Mo\SpeechBundle\Entity\Speech;

/**
 * Manages speeches.
 */
class SpeechController
{
    /**
     * @var \Mo\SpeechBundle\Repository\SpeechRepository
     */
    private $speechRepository;

    /**
     * @var \Mo\SpeechBundle\Repository\IdeaRepository
     */
    private $ideaRepository;

    /**
     * @var \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface
     */
    private $templating;

    /**
     * @param \Mo\SpeechBundle\Repository\SpeechRepository $speechRepository
     * @param \Mo\SpeechBundle\Repository\IdeaRepository $ideaRepository
     * @param \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface $templating
     */
    public function __construct(SpeechRepository $speechRepository, IdeaRepository $ideaRepository, EngineInterface $templating)
    {
        $this->speechRepository = $speechRepository;
        $this->ideaRepository = $ideaRepository;
        $this->templating = $templating;
    }

    /**
     * Shows speech.
     *
     * @param \Mo\SpeechBundle\Entity\Speech $speech
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function showAction(Speech $speech)
    {           
        return $this->templating->renderResponse('MoSpeechBundle:Speech:show.html.twig', compact('speech'));
    }

    /**
     * Shows list of speeches filtered by idea.
     *
     * @param \Mo\SpeechBundle\Entity\Idea $idea
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function listByIdeaAction(Idea $idea)
    {
        $speeches = $this->speechRepository->findByIdea($idea);
        $ideas = $this->ideaRepository->findAll();

        return $this->templating->renderResponse('MoSpeechBundle:Speech:list.html.twig', compact('speeches', 'idea', 'ideas'));
    }

    /**
     * Shows list of all speeches.
     *
     * @return \Symfony\Component\HttpFoundation\Response
     */
    public function listAction()
    {
        $speeches = $this->speechRepository->findAll();
        $ideas = $this->ideaRepository->findAll();

        return $this->templating->renderResponse('MoSpeechBundle:Speech:list.html.twig', compact('speeches', 'ideas'));
    }
}

OK, now I'm sure that behavior of my controller is specified and I can move forward. But I have another problem.

I've used mock of repository for controller spec and now I want to write spec for repository itself:

<?php

namespace Mo\SpeechBundle\Repository;

use Doctrine\ORM\EntityRepository;
use Mo\SpeechBundle\Entity\Idea;

class SpeechRepository extends EntityRepository
{
    /**
     * Finds all speeches by specified idea.
     *
     * @param \Mo\SpeechBundle\Entity\Idea $idea
     *
     * @return array
     */
    public function findByIdea(Idea $idea)
    {
        return $this
            ->createQueryBuilder('s')
            ->leftJoin('s.ideas', 'i')
            ->where('i = :idea')
            ->setParameters(compact('idea'))
            ->getQuery()
            ->getResult()
        ;
    }
}

But specs describe behavior, as I understood. How correctly test repository that it really returns what I need, in my case speeches by ideas.

Should I consider to create functional test with xUnit tool (PHPUnit in PHP world)? Or I wri te spec which describes that my repository correctly creates query? Or can I just use Behat for all app and don't pay attention to this problem.


Solution

  • I have spent a week for analysis of this question and found satisfactory answer.

    The phpspec only specify behavior of our objects. Nothing more. We can't create functional tests with them.

    So, we have two ways to test our functionality:

    1. Use PHPUnit to write functional tests for modules and system itself.
    2. Use Behat to describe features of our application.

    PHPUnit, other similar framework and Behat have their pitfalls and strong sides.

    What to use, can decide only a developer.