Login to bookmark this video
Buy Access to Course
10.

Mad Test Debugging

Share this awesome video!

|

Keep on Learning!

34 Comments

Sort By
Login or Register to join the conversation

Hi there!! So.. when i use guzzlehttp/guzzle v6.* the code of resource/ApiTesterCase(from the start directory) makes phpStorm angry :P. i read some documentation as of the upgrades of guzzlehttp v5 and v6 but instead of fixing/changing the code i just downgraded the package to v 5. Should i bother fixing the changes to the current version for now? I'm not sure if this is a real or rehtorical question..

thanks in advance.

1 | Reply |

Hey emm!

Haha, for the purposes of learning the REST stuff, I would probably not bother upgrading. However, we actually *do* upgrade to Symfony 3 and Guzzle 6 between episode 3 and 4... so episode 4 has all the latest stuff (if you download its course code): http://knpuniversity.com/sc.... So, if you want, that should make upgrading your ApiTestCase to Guzzle 6 pretty easy :).

Cheers!

1 | Reply |

I downloaded the ApiTestCase from episode 4 as I am using Guzzle veriosn 6, however when I run PHPUnit test I get the following error:

Fatal error: Uncaught Declaration of AppBundle\Test\ApiTestCase::onNotSuccessfulTest(Exception $e) should be compatible with PHPUnit\Framework\TestCase::onNotSuccessfulTest(Throwable $t)

And I am also getting the angry errors from PHPStorm regarding the Guzzle paths ;)

use GuzzleHttp\Message\AbstractMessage;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Subscriber\History;

| Reply |

Hey Shaun,

The message is clear enough - method's signature was changed, so you need to change "onNotSuccessfulTest(Exception $e)" to "onNotSuccessfulTest(Throwable $t)" as in parent class.

About the second problem, try to sync vendor/ directory, i.e. right click on vendor/ dir and select "Synchronize 'vendor'". If it does not help, try to re-execute "composer install" or remove vendor/ dir manually and re-run "composer install" again.

Cheers!

| Reply |
Shaun T. avatar Shaun T. Victor 7 years ago edited

Thanks victor

The issue I am now having is that because I am using Symfony 4, the route for the API is not being picked up and I am getting following error:

HTML Summary (h1 and h2):
Symfony Exception
ResourceNotFoundException NotFoundHttpException
HTTP 404 Not Found
No route found for "POST /app_test.php/api/programmers"

| Reply |

Hey Shaun,

First of all, try to clear the cache. Also, make sure you use the correct namespace of the Route, it should be:

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

And try to debug a bit with "bin/console debug:router", do you see this route in the list?

Cheers!

| Reply |
Diaconescu avatar Diaconescu 6 years ago

I tried to modify debugResponse method like so:
protected function debugResponse(ResponseInterface $response){
foreach ($response->getHeaders() as $name => $values) {
$this->printDebug(sprintf('%s: %s', $name, implode(', ', $values)));
}
$body = (string) $response->getBody();

$contentType = $response->getHeader('Content-Type');
$contentType = $contentType[0];
if ($contentType == 'application/json' || strpos($contentType, '+json') !== false) {
$data = json_decode($body);
if ($data === null) {
// invalid JSON!
$this->printDebug($body);
} else {
// valid JSON, print it pretty
$this->printDebug(json_encode($data, JSON_PRETTY_PRINT));
}
} else {
// the response is HTML - see if we should print all of it or some of it
$isValidHtml = strpos($body, '</body>') !== false;

if ($isValidHtml) {
$this->printDebug('');
$crawler = new Crawler($body);

// very specific to Symfony's error page
$isError = $crawler->filter('.trace-line ')->count() > 0
|| strpos($body, 'Symfony Exception') !== false;
if ($isError) {
$this->printDebug('There was an Error!!!!');
$this->printDebug('');
} else {
$this->printDebug('HTML Summary (h1 and h2):');
}

foreach ($crawler->filter('.exception-message-wrapper h1')->extract(array('_text')) as $text) {
$text = $this->removeLineBreaks($text);
if ($isError) {
$this->printErrorBlock($text);
} else {
$this->printDebug($text);
}
}
foreach ($crawler
->filter('.trace-line')
->first()
->extract(array('_text')) as $text
){
$text = $this->removeLineBreaks($text);
if ($isError) {
$this->printErrorBlock($text);
} else {
$this->printDebug($text);
}
}

/*
* When using the test environment, the profiler is not active
* for performance. To help debug, turn it on temporarily in
* the config_test.yml file (framework.profiler.collect)
*/
$profilerUrl = $response->getHeader('X-Debug-Token-Link');
if ($profilerUrl) {
$fullProfilerUrl = $response->getHeader('Host')[0].$profilerUrl[0];
$this->printDebug('');
$this->printDebug(sprintf(
'Profiler URL: <comment>%s</comment>',
$fullProfilerUrl
));
}

// an extra line for spacing
$this->printDebug('');
} else {
$this->printDebug($body);
}
}
}

and onNotSuccessfulTest like so: protected function onNotSuccessfulTest(Throwable $t){
if ($lastResponse = $this->getLastResponse()) {
$this->printDebug('');
$this->printDebug('<error>Failure!</error> when making the following request:');
$this->printLastRequestUrl();
$this->printDebug('');

$this->debugResponse($lastResponse);
}

throw $t;
}

And I created function
protected function removeLineBreaks($text){
// remove line breaks so the message looks nice
$text = str_replace("\n", ' ', trim($text));
// trim any excess whitespace "foo bar" => "foo bar"
$text = preg_replace('/(\s)+/', ' ', $text);

return $text;
}

Probably I misjudge something but i don't see the errors that Symfony Flex raise like 'Argument 1 passed to App\Entity\Programmer::setUser() must be an instance of App\Entity\User or null, string given, called in ...'
in console when I run: APP_ENV=test bin/phpunit src/Tests/Controller/API/ProgrammerControllerTest.php

The fragments of the page I want to see in console is not like the page that I see in web browser when in WebBrowser/ProgrammerController in new method instead of $programmer->setUser($this->getUser()); i
put $programmer->setUser('nn') and submit the form ?

Can I print in console ResponseInterface $response argument in debugResponse method? Is there some configuration I must made in Flex to make happen what is seen in this video?

| Reply |

This looks like a duplicate of this comment: https://symfonycasts.com/sc... - follow the thread there.

Cheers!

| Reply |
Diaconescu avatar Diaconescu 6 years ago edited

Before I asked I had tried this:


        protected function onNotSuccessfulTest(Throwable $t){
		if ($lastResponse = $this->getLastResponse()) {
			$this->printDebug('');
			$this->printDebug('<error>Failure!</error> when making the following request:');
			$this->printLastRequestUrl();
			$this->printDebug('');

			$this->debugResponse($lastResponse);
		}


		throw $t;
	}

But the stack trace is nowhere to be seen

| Reply |

Hey Diaconescu,

Your questions difficult to track because you asked them in different comments and even in threads :)

This looks good to me at the first sight. Hm, are you sure it is not printed? Because sometimes it might be printed in colors that difficult to spot in your terminal. Also, are you sure you get to this onNotSuccessfulTest() method during the execution? And are you sure you get exactly into that if statement? Try to debug it first using "die" statement like:


        protected function onNotSuccessfulTest(Throwable $t){
                die('DEBUG onNotSuccessfulTest');
		if ($lastResponse = $this->getLastResponse()) {
                        die('DEBUG if');
			$this->printDebug('');
			$this->printDebug('<error>Failure!</error> when making the following request:');
			$this->printLastRequestUrl();
			$this->printDebug('');

			$this->debugResponse($lastResponse);
		}

		throw $t;
	}

If you get the first die, comment it out and check if you see the second. This way you can find the problem and probably add more debug statements to better understand why. Also, it would be cool if you your terminal have search. If not, you can copy/paste the whole terminal output somewhere in a text editor and search for "DEBUG" keyword there.

Cheers!

| Reply |
Diaconescu avatar Diaconescu Victor 6 years ago

I did some debuggings. OnNotSuccessful test is surely hit.
After I put debug statement in getLastResponse method:
var_dump(self::$history)die() as the first line of it and I see that this static variable is never populated because this line prints:
array(0) {
}
This is the reason why if statement never executes.
The only culprit I may think is phpunit settings, but I can't come up with the correct ones.
As may I see setUBeforeClass is responsible to populate history static variable.
So t looks like this:
public static function setUpBeforeClass(){
$handler = HandlerStack::create();

$handler->push(Middleware::history(self::$history));

self::$staticClient = new Client([
'base_uri' => 'http://localhost:8000',
'http_errors' => false,
'handler' => $handler
]);

self::bootKernel();
}
Configuration in phpunit.xml.dist is the standard one:

<phpunit xmlns:xsi="http://www.w3.org/2001/XMLS..." xsi:nonamespaceschemalocation="http://schema.phpunit.de/6...." backupglobals="false" colors="true" bootstrap="config/bootstrap.php">
<php>
<ini name="error_reporting" value="-1"/>
<env name="APP_ENV" value="test"/>
<env name="SHELL_VERBOSITY" value="-1"/>
</php>

<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>

<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>

<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>
</phpunit>

I use Symfony 4.2 right now so I created .env.local file where I put APP_ENV=test
And I filled in env.test DATABASE_URL=mysql://root:mysql@127.0.0.1:3306/RestApiSiteTest

| Reply |

Hey Diaconescu,

<blockquote>

I did some debuggings. OnNotSuccessfulTest() is surely hit
</blockquote>

That's great, it means it should work.

<blockquote>
After I put debug statement in getLastResponse() method:
var_dump(self::$history)die() as the first line of it and I see that this static variable is never populated because this line prints:
array(0) {
}
</blockquote>

Hm, it might depend on what test you're running. Because you added "die;" statement - it means you run only first failed test. You can try without "die;", probably further tests will populate self::$history property, but it also depends on do you send any requests or no in tests. If no - I think it makes sense that it's empty. Or try to use "--filter" option to execute exactly the test that sends requests.

Btw, what version of PHPUnit do you use?

Hm, btw, it's a little known fact, but your .env.local file does not read in test environment as far as I know, see config/bootstrap.php file for more context. It means, that you need to have another one for test env that's called .env.test.local. But if you only change APP_ENV=test in your .env.local - it should be not important as far as you have APP_ENV is set in your phpunit.xml.dist:


<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.5/phpunit.xsd"
         backupGlobals="false"
         colors="true"
         bootstrap="config/bootstrap.php"
>
    <php>
        <ini name="error_reporting" value="-1" />
        <env name="APP_ENV" value="test" />
        <env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
    </php>

    <!-- other config here ... -->

</phpunit>

Also, double check you have colors="true" in your phpunit.xml.dist.

Cheers!

| Reply |
Diaconescu avatar Diaconescu 6 years ago

I ry to rework this tutorial with Symfony 4, Flex, php7.2, phpunit 6.5. I tried yor code from episode 4 but phpunit raise the error "Declaration of App\Tests\ApiTestCase::onNotSuccessfulTest(Exception $e) should be compatible with PHPUnit\Framework\TestCase::onNotSuccessfulTest(Throwable $t)"
How must look ApiTestCase to get the Symfony exception page using Throwable instead of exceptions?

| Reply |

Hey Diaconescu,

It's an easy fix! As you can see from the error message, the onNotSuccessfulTest() method is not compatible with one from the newer version - that was a BC break from PHPUnit. So the fix: just change ours ApiTestCase::onNotSuccessfulTest(\Exception $e) signature to ApiTestCase::onNotSuccessfulTest(\Throwable $t), i.e. allow a Throwable object to be passed there. And that's it :)

Cheers!

| Reply |
Diaconescu avatar Diaconescu Victor 6 years ago edited

I tried to modify debugResponse method like so:


protected function debugResponse(ResponseInterface $response){
foreach ($response->getHeaders() as $name => $values) {
    $this->printDebug(sprintf('%s: %s', $name, implode(', ', $values)));
}
$body = (string) $response->getBody();

$contentType = $response->getHeader('Content-Type');
$contentType = $contentType[0];
if ($contentType == 'application/json' || strpos($contentType, '+json') !== false) {
$data = json_decode($body);
if ($data === null) {
// invalid JSON!
$this->printDebug($body);
} else {
// valid JSON, print it pretty
$this->printDebug(json_encode($data, JSON_PRETTY_PRINT));
}
} else {
// the response is HTML - see if we should print all of it or some of it
$isValidHtml = strpos($body, '</body>') !== false;

if ($isValidHtml) {
$this->printDebug('');
$crawler = new Crawler($body);

// very specific to Symfony's error page
$isError = $crawler->filter('.trace-line ')->count() > 0
|| strpos($body, 'Symfony Exception') !== false;
if ($isError) {
$this->printDebug('There was an Error!!!!');
$this->printDebug('');
} else {
$this->printDebug('HTML Summary (h1 and h2):');
}

foreach ($crawler->filter('.exception-message-wrapper h1')->extract(array('_text')) as $text) {
$text = $this->removeLineBreaks($text);
if ($isError) {
$this->printErrorBlock($text);
} else {
$this->printDebug($text);
}
}
foreach ($crawler
->filter('.trace-line')
->first()
->extract(array('_text')) as $text
){
$text = $this->removeLineBreaks($text);
if ($isError) {
$this->printErrorBlock($text);
} else {
$this->printDebug($text);
}
}

/*
* When using the test environment, the profiler is not active
* for performance. To help debug, turn it on temporarily in
* the config_test.yml file (framework.profiler.collect)
*/
$profilerUrl = $response->getHeader('X-Debug-Token-Link');
if ($profilerUrl) {
$fullProfilerUrl = $response->getHeader('Host')[0].$profilerUrl[0];
$this->printDebug('');
$this->printDebug(sprintf(
'Profiler URL: <comment>%s</comment>',
$fullProfilerUrl
));
}

// an extra line for spacing
$this->printDebug('');
} else {
$this->printDebug($body);
}
}
}

and onNotSuccessfulTest like so:


protected function onNotSuccessfulTest(Throwable $t){
if ($lastResponse = $this->getLastResponse()) {
$this->printDebug('');
$this->printDebug('<error>Failure!</error> when making the following request:');
$this->printLastRequestUrl();
$this->printDebug('');

$this->debugResponse($lastResponse);
}

throw $t;
}

And I created function


protected function removeLineBreaks($text){
// remove line breaks so the message looks nice
$text = str_replace("\n", ' ', trim($text));
// trim any excess whitespace "foo bar" => "foo bar"
$text = preg_replace('/(\s)+/', ' ', $text);

return $text;
}

Probably I misjudge something but i don't see the errors that Symfony Flex raise like 'Argument 1 passed to App\Entity\Programmer::setUser() must be an instance of App\Entity\User or null, string given, called in ...'
in console when I run: APP_ENV=test bin/phpunit src/Tests/Controller/API/ProgrammerControllerTest.php

The fragments of the page I want to see in console is not like the page that I see in web browser when in WebBrowser/ProgrammerController in new method instead of $programmer->setUser($this->getUser()); i
put $programmer->setUser('nn') and submit the form ?

Can I print in console ResponseInterface $response argument in debugResponse method? Is there some configuration I must made in Flex to make happen what is seen in this video?

| Reply |

Hey Diaconescu,

Wait, you don't see the errors of Symfony Flex? Or you don't see errors of PHPUnit? Because Symfony Flex does not relate to the error you mentioned like "Argument 1 passed to App\Entity\Programmer::setUser() must be an instance of App\Entity\User or null, string given, called in...".

Could you tell me if the tests work and pass before you made your changes?

Cheers!

| Reply |
Diaconescu avatar Diaconescu Victor 6 years ago

Ok, I wasn't clear enough.
In your screencast after ProgrammerControllerTest extends ApiTestCase when some phpunit test fails in console appear the actual reason of this failure. In your example this was '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.'
In my case if I run: 'APP_ENV=test bin/phpunit src/Tests/Controller/API/ProgrammerControllerTest.php' i only see that respective test fails:
1) App\Tests\Controller\API\ProgrammerControllerTest::testPOST
Failed asserting that 404 matches expected 201.
I don't see the reason in phpunit console.
I'am wrong If i say that the html part that is hacked into the console in your video are fragments from the 500 error page raised by Symfony when something wrong happen? If that is the case not only onNoSuccessfulTest method must be changed to match the signature of the new onNoSuccessfulTest phpunit method, but in case of Symfony4 to match the behaviour from these video debugResponse method must be changed because 500 error page is not the same as before. I triggered the same exception on web part and I inspected the 500 html page that was raised The methods that i write in previous asking is intending to extract only I considered to be the important part.
Obviously something eluded me because on API side in phpunit cosole I see only that ProgrammerControllerTest::testPOST method fails. That's not an eror. But I don't see the reason of this failure in console.
Where I am wrong?
The actual code would be more helpfull? I have a git repository with all steps that i made in cronological order from the creation of the entities with bin/console make to changes I made in controllers and forms and so on
testPOST method give the correct message failure, there's no errors, only the failure message, so it works with modifications i specified, but is not helpful enough because doesn't have any explanatory lines like in your video.
I hope you excuse me. I don't want to torment you.
Cheers and Happy new year

| Reply |

Hey Diaconescu!

Yes, I think I may be understand you :). Specifically the problems are:

> onNoSuccessfulTest method must be changed to match the signature of the new onNoSuccessfulTest phpunit method,

and

> the behaviour from these video debugResponse method must be changed because 500 error page is not the same as before

I can't confirm that you're 100% correct about these, but they both make sense :). If you're able to push your example code, that *would* indeed help us. Also, have you been able to change the debugResponse() method to work with the new markup? Or are you having some issues with that?

Cheers!

| Reply |

Cheers!
I think I resolved debugResponse() method. I raised the same kind error on web interface and I hack the markup I found there. To raise this error I replace $programmer->setUser($this->getUser()) in new ProgrammerControoler method with $programmer->setUser('nnn') and I inspected the markup to see how it looks like.
You may see the actual code at https://github.com/petre-sy.... Dwnload the zip archive from there.
I tried to describe precisely in the last five commits what I reused from your code and what I replaced and also a little debugging. I hope is nothing ambiguous now.
Thank you.

Did you find the repo? You can download the code?

1 | Reply |

Hey Diaconescu!

Ah, it makes perfect sense! Nice work - and thanks for doing that :). I CAN see the repo - hopefully it will help other people. I see that most of the fixes were on this commit: https://github.com/petre-sy...

Cheers!

| Reply |

Do you see any fix to correct the problem that this code has?

| Reply |

Hi Diaconescu!

Ah, I think I misunderstood you! Apologies! I thought you were telling me that you DID successfully "update" the code in that GitHub repository for the new exception page format. But, it sounds like that is not true. With your modified code, you are still not seeing the errors correctly in your terminal, is that correct? Do you see anything at all? Or still just an error? I can definitely look into helping :).

Cheers!

| Reply |

I added recently two new commits because I added the new panther browser test to the mix.
I don't see anything at all. Is just the normal failing test message. like this:
Testing Project Test Suite
array(0) {
}
F 1 / 1 (100%)

Time: 2.33 seconds, Memory: 30.00MB

There was 1 failure:

1) App\Tests\Controller\Api\ProgrammerControllerTest::testPOST
Failed asserting that 500 matches expected 201.

/home/petrero/www/Symfony/RestAPISite/tests/Controller/Api/ProgrammerControllerTest.php:27

There's no trace of 500 error page here. Indeed in console where run panther browser appear the reason respectivelly: Uncaught PHP Exception Doctrine\DBAL\Exception\NotNullConstraintViolationException: "An exception occurred while executing 'INSERT INTO programmer (nickname, avatar_number, tag_line, power_level, user_id) VALUES (?, ?, ?, ?, ?)' with params ["ObjectOrienter", 5, "

1 | Reply |

I added two new commits.
I have only the failing normal message that phpunit normally give. I don't see which line in ProgrammerController is the one that makes the code to explode. In other words i don't see anything that's scrapped from 500 symfony error page.

| Reply |

Hi again Diaconescu!

Sorry for being slow! I've just cloned your code and got it working - you already did all the hard work and were very close! Here is the PR: https://github.com/petre-symfony/RestApiSymfony4/pull/1

I really only needed to do 2 things:

1) In the test function, you were instantiating your own Client object. You need to use $this->client instead. Otherwise, the static history property will never be populated in ApiTestCase and so we don't have access to the Response to print it.

2) Once I did this, onNotSuccessfulTest was successfully being called and the $lastResponse variable was set. But THEN I got an error that the symfony/css-selector is not installed. Indeed, inside the debugResponse function, we're using some code with the $crawler that needs the symfony/css-selector to be installed. Once I did that, it all worked!

Let me know if this makes sense!

Cheers!

| Reply |

As soon as my eyes landed on my test code maked perfect sense. That was a very infurating mistake. I was so entranched in APITest code that I saw nothing else.
Thank you. It works now, indeed

| Reply |
Lijana Z. avatar Lijana Z. 6 years ago

where can we get that function which extract info from error with html ?

| Reply |

Hey Lijana Z.

You can find those debugging methods inside src/AppBundle/Test/ApiTestCase.php

Cheers!

1 | Reply |
Default user avatar Nacer 7 years ago

Hi all, my Phpstrom don't like these lines in ApiTestCase calss

use GuzzleHttp\Message\AbstractMessage;
use GuzzleHttp\Message\ResponseInterface;
use GuzzleHttp\Subscriber\History;

| Reply |

Hey Nacer,

What do you mean? Can't PhpStorm find these classes or these classes just are not used in the class? If the 2nd, i.e. you don't use those classes - just remove the namespaces. But if the first one - please, make sure you've installed dependencies with "composer install". Also, what version of Guzzle do you use? Probably you use a different major version, so those namespaces were changed and you need to use the new ones.

Cheers!

| Reply |

I'm on GuzzleHttp 6, I' think the file in resources need to be updated

| Reply |

Hey Nacer!

Ah yes. So, this tutorial uses version *5* of Guzzle. But, before episode 4 (https://knpuniversity.com/s..., we upgraded to Guzzle 6. If you download the start code for that tutorial, you'll find an ApiTestCase that works with version 6 :).

Cheers!

| Reply |
Default user avatar Lipiluk 7 years ago

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.

| Reply |

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!

| Reply |

Delete comment?

Share this comment

astronaut with balloons in space

"Houston: no signs of life"
Start the conversation!