Buy

Mad Test Debugging

When 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():

205 lines src/AppBundle/Test/ApiTestCase.php
... 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().

200 lines src/AppBundle/Test/ApiTestCase.php
... 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:

200 lines src/AppBundle/Test/ApiTestCase.php
... 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:

200 lines src/AppBundle/Test/ApiTestCase.php
... 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:

33 lines src/AppBundle/Controller/Api/ProgrammerController.php
... 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:

209 lines src/AppBundle/Test/ApiTestCase.php
... 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!

Seeing the Exception Stacktrace!

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.

Leave a comment!

  • 2016-08-01 weaverryan

    Hi there!

    Yea, great question! This is a personal preference of mine. The reason is that Guzzle is the standard in the PHP world for making HTTP/API requests. Symfony's client/crawler is quite good, but the crawler (specifically) is useful for crawling HTML pages - it doesn't serve you any purpose when making API requests. So, I usually think it makes more sense to use Guzzle, since you'll probably also use it in the real-world to make API requests to other services.

    However, there is one advantage that Symfony's client has over Guzzle. Because (with the Symfony client) you are *not* making real HTTP requests (you are making "fake" requests into Symfony's kernel), you can potentially do 2 interesting things. First, you could change some setting in the container right in your test class, make the request class, and your code will use that setting. Second, and probably more interesting, you can turn on the profiler and get information from the profiler (e.g. "was an email sent?"). That's not enough for me to want to use it however :).

    Thanks for the question - hope that clarifies!

    Cheers!

  • 2016-07-30 Lipiluk

    I would like to ask, why are you using Guzzle instead of in-built Symfony's client/crawler? Are there any advantages or disadvantages of using those both? As far as I can see Symfony's offer programmers such an ability. Thank you for any reply.