PHPUnit with phpspec 23/09/2015
PHPUnit is a port of jUnit, its name might be deceptive: it allows you to write any type of tests (unit, but also functional, system, integration, end to end, acceptance, etc).
phpspec was at first a port of rspec, it can be considered as a unit test framework that enforces practices it considers best.
Note: read more about phpspec.
In this article, we'll see how to use both tools together in a same project.
Fortune: our example
We're going to build part of a fortune application for our example, more precisely we're going to build a CLI allowing us to save quotes.
To do so, we'll bootstrap a symfony application using the Empty Edition:
composer create-project gnugat/symfony-empty-edition fortune
cd fortune
We'll need to install our test frameworks:
composer require --dev phpunit/phpunit
composer require --dev phpspec/phpspec
Finally we'll configure PHPUnit:
<?xml version="1.0" encoding="UTF-8"?>
<!-- phpunit.xml.dist -->
<!-- http://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit backupGlobals="false" colors="true" syntaxCheck="false" bootstrap="app/bootstrap.php">
<testsuites>
<testsuite name="System Tests">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
The command
Our first step will be to write a system test describing the command:
<?php
// tests/Command/SaveQuoteCommandTest.php
namespace AppBundle\Tests\Command;
use AppKernel;
use PHPUnit_Framework_TestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\ApplicationTester;
class SaveQuoteCommandTest extends PHPUnit_Framework_TestCase
{
const EXIT_SUCCESS = 0;
private $app;
protected function setUp()
{
$kernel = new AppKernel('test', false);
$application = new Application($kernel);
$application->setAutoExit(false);
$this->app = new ApplicationTester($application);
}
/**
* @test
*/
public function it_saves_a_new_quote()
{
$exitCode = $this->app->run(array(
'quote:save',
'quote' => 'Nobody expects the spanish inquisition',
));
self::assertSame(self::EXIT_SUCCESS, $exitCode, $this->app->getDisplay());
}
}
Note: Testing only the exit code is called "Smoke Testing" and is a very efficient way to check if the application is broken. Testing the output would be tedious and would make our test fragile as it might change often.
Let's run it:
vendor/bin/phpunit
The tests fails because the command doesn't exist. Let's fix that:
<?php
// src/AppBundle/Command/SaveQuoteCommand.php
namespace AppBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Output\OutputInterface;
class SaveQuoteCommand extends ContainerAwareCommand
{
protected function configure()
{
$this->setName('quote:save');
$this->addArgument('quote', InputArgument::REQUIRED);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln('');
$output->writeln('// Saving quote');
$this->getContainer()->get('app.save_new_quote')->save(
$input->getArgument('quote')
);
$output->writeln('');
$output->writeln(' [OK] Quote saved');
$output->writeln('');
}
}
Then run the test again:
vendor/bin/phpunit
It now fails for a different reason: the service used doesn't exist.
The service
The second step is to write the unit test for the service. With phpspec we can first bootstrap it:
vendor/bin/phpspec describe 'AppBundle\Service\SaveNewQuote'
Then we need to edit it:
<?php
// spec/AppBundle/Service/SaveNewQuoteSpec.php
namespace spec\AppBundle\Service;
use PhpSpec\ObjectBehavior;
use Symfony\Component\Filesystem\Filesystem;
class SaveNewQuoteSpec extends ObjectBehavior
{
const FILENAME = '/tmp/quotes.txt';
const QUOTE = 'Nobody expects the spanish inquisition!';
function let(Filesystem $filesystem)
{
$this->beConstructedWith($filesystem, self::FILENAME);
}
function it_saves_new_quote(Filesystem $filesystem)
{
$filesystem->dumpFile(self::FILENAME, self::QUOTE)->shouldBeCalled();
$this->save(self::QUOTE);
}
}
Time to run the suite:
vendor/bin/phpspec run
phpspec will detect that the tested class doesn't exist and will bootstrap it for us, so we just have to edit it:
<?php
// src/AppBundle/Service/SaveNewQuote.php
namespace AppBundle\Service;
use Symfony\Component\Filesystem\Filesystem;
class SaveNewQuote
{
private $filesystem;
private $filename;
public function __construct(Filesystem $filesystem, $filename)
{
$this->filesystem = $filesystem;
$this->filename = $filename;
}
public function save($quote)
{
$this->filesystem->dumpFile($this->filename, $quote);
}
}
Again, we're going to run our unit test:
vendor/bin/phpspec run
It's finally green! Our final step will be to define our service in the Dependency Injection Container:
# app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: importer.php }
framework:
secret: "%secret%"
services:
app.save_new_quote:
class: AppBundle\Service\SaveNewQuote
arguments:
- "@filesystem"
- "%kernel.root_dir%/cache/quotes"
To make sure everything is fine, let's clear the cache and run the test:
rm -rf app/cache/*
vendor/bin/phpunit
It's Super Green!
Conclusion
As we can see, PHPUnit and phpspec can work perfectly well together.
Of course we could write our unit test in a similar manner with PHPUnit:
<?php
// tests/Service/SaveNewQuoteTest.php
namespace AppBundle\Tests\Service;
use AppBundle\Service\SaveNewQuote;
use PHPUnit_Framework_TestCase;
class SaveNewQuoteTest extends PHPUnit_Framework_TestCase
{
const FILENAME = '/tmp/quotes.txt';
const QUOTE = 'Nobody expects the spanish inquisition!';
private $filesystem;
private $saveNewQuote;
protected function setUp()
{
$this->filesystem = $this->prophesize('Symfony\Component\Filesystem\Filesystem');
$this->saveNewQuote = new SaveNewQuote($this->filesystem->reveal(), self::FILENAME);
}
/**
* @test
* @group unit
*/
public function it_saves_new_quote()
{
$this->filesystem->dumpFile(self::FILENAME, self::QUOTE)->shouldBeCalled();
$this->saveNewQuote->save(self::QUOTE);
}
}
And run it separately:
vendor/bin/phpunit --group=unit
But then we would lose all the advantages of phpspec:
- it adds less overhead (this same test runs in ~20ms with phpspec, and ~80ms with PHPUnit)
- it tells you when it thinks you're doing something wrong (typically by making it harder/impossible for you to do it)
- it bootstraps things for you if you follow the TDD workflow (test first, then code)
Reference: see the phpspec reference article