[How2Tips] Spec a Symfony command

Published on

Sep 5, 2018

Nicolas is a Symfony developer and trainer at KNPLabs and loves his current mission at i24news.tv as Devops. He shares with you a small and helpful tip on how to use phpspec to spec a symfony command.

When you write a Symfony command, you'd like to be able to test it too. To do so, you can take a look to the official documentation, but unfortunately there are no instructions for phpspec. However, it is possible to spec a Symfony command with phpspec.

The command

First, let's take a look at a simple command which is archiving the blog posts  published a long time ago (defined in a parameter.yml file for instance) :

    namespace AppBundle\Command;
    
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Output\OutputInterface;
    use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
    
    class ArchiveOutdatedBlogPostCommand extends ContainerAwareCommand
    {
        public function configure()
        {
            $this
                ->setName('app:blog-post:archive-outdated')
                ->setDescription('Archive the outdated blog posts')
            ;
        }
    
        public function execute(InputInterface $input, OutputInterface $output)
        {
            $container = $this->getContainer();
            $ttl = $container->getParameter('published_blog_post_ttl');
    
            $publicationDate = new \DateTime();
            $publicationDate->modify(sprintf('-%d days', $ttl));
    
            $output->writeln(sprintf('Archiving blog posts published more than %d days ago...', $ttl));
    
            $count = $container->get('app.repository.blog_post')
                ->archiveOutdated($publicationDate);
    
            $output->writeln(sprintf('%d blog post(s) archived.', $count));
    
            return 0;
        }
    }

For this example, we can assert that the `archiveOutdated` function is called on  the blog post repository with the correct parameter. We can also assert that the  info messages are written on the output. All a command needs to be executed, is an `InputInterface` and an `OuputInterface`. If we look into the console component's `Input` and `Output` directories, we can see that there are many classes implementing the above interfaces. For this example, we'll use the `BufferedOutput` class to get the messages written to the output, and the `ArgvInput` class for the input (which represents an input coming from the CLI arguments, but as we don't have any arguments for this command it will be even simpler).

The spec

Finally, we're able to create a spec file for our command, which will use a mock of the `BlogPostRepository` and a `Container` which will have the mocked repo and the `published_blog_post_ttl` parameter :

namespace spec\\AppBundle\\Command;

use PhpSpec\\ObjectBehavior;
use Prophecy\\Argument;
use Prophecy\\Prophet;
use Symfony\\Component\\Console\\Input\\ArgvInput;
use Symfony\\Component\\Console\\Output\\BufferedOutput;
use Symfony\\Component\\DependencyInjection\\Container;

class ArchiveOutdatedBlogPostCommandSpec extends ObjectBehavior
{
    private $prophet;
    private $count = 10; // the number of blog posts that should be archived
    private $ttl = 270;

    public function let()
    {
        $this->prophet = new Prophet();
    }

    public function letGo()
    {
        unset($this->prophet);
    }

    public function it\_archive\_outdated\_blog\_posts()
    {
        // mock the repository and assert that the archiveOutdated function is
        // called with the expected parameter
        $repo = $this->prophet->prophesize('AppBundle\\Repository\\BlogPostRepository');
        $repo->archiveOutdated(Argument::that(array($this, 'assertPublicationDate')))
            ->willReturn($this->count);

        // instantiate the input and output interfaces
        $input = new ArgvInput();
        $output = new BufferedOutput();

        // create the container with the repo mock and the ttl parameter
        $container = new Container();
        $container->set('app.repository.blog\_post', $repo->reveal());
        $container->setParameter('published\_blog\_post\_ttl', $this->ttl);

        $this->setContainer($container);
        $this->execute($input, $output)
            ->shouldReturn(0);

        $this->assertOutput($output);
    }

    /\*\*
     \* Assert that the repository function is called with the expected parameter.
     \*/
    public function assertPublicationDate(\\DateTime $publicationDate)
    {
        $interval = $publicationDate->diff(new \\DateTime());

        if ($interval->days !== $this->ttl) {
            throw new \\InvalidArgumentException(sprintf(
                'Expected %d days ttl, got %d',
                $this->ttl,
                $interval->days
            ));
        }

        return true;
    }

    /\*\*
     \* Assert that the text written on the output is as expected.
     \*/
    private function assertOutput(BufferedOutput $output)
    {
        $buffer = $output->fetch();

        if (false === strpos($buffer, sprintf('Archiving contents (articles, timelines and videos) published more than %d days ago...', $this->ttl))) {
            throw new \\RuntimeException('Unexpected command output');
        }

        if (false === strpos($buffer, sprintf('%d content(s) archived.', $this->count))) {
            throw new \\RuntimeException('Unexpected command output');
        }
    }
}

Then run the spec :

$ vendor/bin/phpspec run

and voilà! It is possible to use other implementations of the input / output interfaces to test more complicated commands (eg when dealing with arguments). Thanks to @docteurklein who put me on the right track for this problem.

You want to join the team ? Send us an email to hello@knplabs.com

Written by

Nicolas Mure
Nicolas Mure

Comments