If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeWhen we mess up in a web app, we see Symfony's giant exception page. I want that same experience when I'm building an API.
At the root of the project there's a resources/
directory with an ApiTestCase.php
file. This has all the same stuff as our ApiTestCase
plus some pretty sweet new debugging stuff.
Copy this and paste it over our class.
First, check out onNotSuccessfulTest()
:
... lines 1 - 64 | |
protected function onNotSuccessfulTest(Exception $e) | |
{ | |
if (self::$history && $lastResponse = self::$history->getLastResponse()) { | |
$this->printDebug(''); | |
$this->printDebug('<error>Failure!</error> when making the following request:'); | |
$this->printLastRequestUrl(); | |
$this->printDebug(''); | |
$this->debugResponse($lastResponse); | |
} | |
throw $e; | |
} | |
... lines 78 - 205 |
If you have a method with this name, PHPUnit calls it whenever a test fails. I'm using it to print out the last response so we can see what just happened.
I also added a few other nice things, like printLastRequestUrl()
.
... lines 1 - 90 | |
protected function printLastRequestUrl() | |
{ | |
$lastRequest = self::$history->getLastRequest(); | |
if ($lastRequest) { | |
$this->printDebug(sprintf('<comment>%s</comment>: <info>%s</info>', $lastRequest->getMethod(), $lastRequest->getUrl())); | |
} else { | |
$this->printDebug('No request was made.'); | |
} | |
} | |
... lines 101 - 200 |
Next up is debugResponse()
use it if you want to see what a Response looks like:
... lines 1 - 101 | |
protected function debugResponse(ResponseInterface $response) | |
{ | |
$this->printDebug(AbstractMessage::getStartLineAndHeaders($response)); | |
$body = (string) $response->getBody(); | |
... lines 106 - 172 | |
} | |
... lines 174 - 200 |
This crazy function is something I wrote - it knows what Symfony's error page looks like and tries to extract the important parts... so you don't have to stare at a giant HTML page in your terminal. I hate that. It's probably not perfect - and if you find an improvement and want to share it, you'll be my best friend.
And finally, whenever this class prints something, it's calling printDebug()
. And right now, it's about as dull as you can get:
... lines 1 - 179 | |
protected function printDebug($string) | |
{ | |
echo $string."\n"; | |
} | |
... lines 184 - 200 |
I think we can make that way cooler. But first, with this in place, it should print out the last response so we can see the error:
php bin/phpunit -c app src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
Ah hah!
Catchable Fatal Error: Argument 1 passed to Programmer::setUser() must
be an instance of AppBundle\Entity\User, null given in ProgrammerController.php
on line 29.
So the problem is that when we delete our database, we're also deleting our hacked-in weaverryan
user:
... lines 1 - 17 | |
public function newAction(Request $request) | |
{ | |
... lines 20 - 23 | |
$programmer->setUser($this->findUserByUsername('weaverryan')); | |
... lines 25 - 30 | |
} | |
... lines 32 - 33 |
Let's deal with that in a second - and do something cool first. So, remember how some of the app/console
commands have really pretty colored text when they print? Well, we're not inside a console command in PHPUnit, but I'd love to be able to print out with colors.
Good news! It turns out, this is really easy. The class that handles the styling is called ConsoleOutput
, and you can use it directly from anywhere.
Start by adding a private $output
property that we'll use to avoid creating a bunch of these objects. Then down in printDebug()
, say if ($this->output === null)
then $this->output =
new ConsoleOutput();. This is the $output
variable you're passed in a normal Symfony command. This means we can say $this->output->writeln()
and pass it the $string
:
... lines 1 - 12 | |
use Symfony\Component\Console\Output\ConsoleOutput; | |
... lines 14 - 15 | |
class ApiTestCase extends KernelTestCase | |
{ | |
... lines 18 - 29 | |
/** | |
* @var ConsoleOutput | |
*/ | |
private $output; | |
... lines 34 - 184 | |
protected function printDebug($string) | |
{ | |
if ($this->output === null) { | |
$this->output = new ConsoleOutput(); | |
} | |
$this->output->writeln($string); | |
} | |
... lines 193 - 207 | |
} |
I'm coloring some things already, so let's see this beautiful art! Re-run the test:
php bin/phpunit -c app src/AppBundle/Tests/Controller/Api/ProgrammerControllerTest.php
Hey! That error is hard to miss!
Ok, one more debugging trick. What if we really need to see the full stacktrace? The response headers are printed on top - and one of those actually holds the profiler URL for this request. And to be even nicer, my debug code is printing that at the bottom too.
Pop that into the browser. This is the profiler for that API request. It has cool stuff like the database queries, but most importantly, there's an Exception
tab - you can see the full, beautiful exception with stacktrace. This is huge.
// composer.json
{
"require": {
"php": ">=5.3.3",
"symfony/symfony": "2.6.*", // v2.6.11
"doctrine/orm": "~2.2,>=2.2.3,<2.5", // v2.4.7
"doctrine/dbal": "<2.5", // v2.4.4
"doctrine/doctrine-bundle": "~1.2", // v1.4.0
"twig/extensions": "~1.0", // v1.2.0
"symfony/assetic-bundle": "~2.3", // v2.6.1
"symfony/swiftmailer-bundle": "~2.3", // v2.3.8
"symfony/monolog-bundle": "~2.4", // v2.7.1
"sensio/distribution-bundle": "~3.0,>=3.0.12", // v3.0.21
"sensio/framework-extra-bundle": "~3.0,>=3.0.2", // v3.0.7
"incenteev/composer-parameter-handler": "~2.0", // v2.1.0
"hautelook/alice-bundle": "0.2.*", // 0.2
"jms/serializer-bundle": "0.13.*" // 0.13.0
},
"require-dev": {
"sensio/generator-bundle": "~2.3", // v2.5.3
"behat/behat": "~3.0", // v3.0.15
"behat/mink-extension": "~2.0.1", // v2.0.1
"behat/mink-goutte-driver": "~1.1.0", // v1.1.0
"behat/mink-selenium2-driver": "~1.2.0", // v1.2.0
"phpunit/phpunit": "~4.6.0" // 4.6.4
}
}