Skip to content

Commit

Permalink
feature #36003 [ErrorHandler][FrameworkBundle] better error messages …
Browse files Browse the repository at this point in the history
…in failing tests (guillbdx)

This PR was squashed before being merged into the 5.1-dev branch.

Discussion
----------

[ErrorHandler][FrameworkBundle] better error messages in failing tests

| Q             | A
| ------------- | ---
| Branch?       | master for features
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #32752
| License       | MIT
| Doc PR        |

Purpose of this PR is to enhance tests by giving a way to report an exception that occured during the processing of the request.

The ErrorHandler will add an X-Debug-Exception, and the assertThat() method of WebTestCase will throw an exception if this header exists and status code is 5xx.

In practice, this adds the "Caused by" section in this example:

```
Time: 374 ms, Memory: 20.00 MB

There was 1 failure:

1) App\Tests\Controller\HomeControllerTest::testC
Failed asserting that the Response has header "Content-Type" with value "application/json".

/srv/symfony/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php:132
/srv/symfony/src/Symfony/Bundle/FrameworkBundle/Test/BrowserKitAssertionsTrait.php:66
/srv/blog/tests/Controller/HomeControllerTest.php:29

Caused by
Exception: This a test exception. in /the/file.php:139
Stack trace:
[...]
```

Commits
-------

0da9469 [ErrorHandler][FrameworkBundle] better error messages in failing tests
  • Loading branch information
fabpot committed Mar 16, 2020
2 parents 1897e03 + 0da9469 commit 693cf11
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 15 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Expand Up @@ -14,6 +14,7 @@ CHANGELOG
* Deprecated *not* setting the "framework.router.utf8" configuration option as it will default to `true` in Symfony 6.0
* Added tag `routing.expression_language_function` to define functions available in route conditions
* Added `debug:container --deprecations` option to see compile-time deprecations.
* Made `BrowserKitAssertionsTrait` report the original error message in case of a failure

5.0.0
-----
Expand Down
Expand Up @@ -11,8 +11,10 @@

namespace Symfony\Bundle\FrameworkBundle\Test;

use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\Constraint\LogicalAnd;
use PHPUnit\Framework\Constraint\LogicalNot;
use PHPUnit\Framework\ExpectationFailedException;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\BrowserKit\Test\Constraint as BrowserKitConstraint;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -28,12 +30,12 @@ trait BrowserKitAssertionsTrait
{
public static function assertResponseIsSuccessful(string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseIsSuccessful(), $message);
self::assertThatForResponse(new ResponseConstraint\ResponseIsSuccessful(), $message);
}

public static function assertResponseStatusCodeSame(int $expectedCode, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message);
self::assertThatForResponse(new ResponseConstraint\ResponseStatusCodeSame($expectedCode), $message);
}

public static function assertResponseRedirects(string $expectedLocation = null, int $expectedCode = null, string $message = ''): void
Expand All @@ -46,42 +48,42 @@ public static function assertResponseRedirects(string $expectedLocation = null,
$constraint = LogicalAnd::fromConstraints($constraint, new ResponseConstraint\ResponseStatusCodeSame($expectedCode));
}

self::assertThat(self::getResponse(), $constraint, $message);
self::assertThatForResponse($constraint, $message);
}

public static function assertResponseHasHeader(string $headerName, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasHeader($headerName), $message);
self::assertThatForResponse(new ResponseConstraint\ResponseHasHeader($headerName), $message);
}

public static function assertResponseNotHasHeader(string $headerName, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message);
self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHasHeader($headerName)), $message);
}

public static function assertResponseHeaderSame(string $headerName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message);
self::assertThatForResponse(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue), $message);
}

public static function assertResponseHeaderNotSame(string $headerName, string $expectedValue, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message);
self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHeaderSame($headerName, $expectedValue)), $message);
}

public static function assertResponseHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message);
self::assertThatForResponse(new ResponseConstraint\ResponseHasCookie($name, $path, $domain), $message);
}

public static function assertResponseNotHasCookie(string $name, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message);
self::assertThatForResponse(new LogicalNot(new ResponseConstraint\ResponseHasCookie($name, $path, $domain)), $message);
}

public static function assertResponseCookieValueSame(string $name, string $expectedValue, string $path = '/', string $domain = null, string $message = ''): void
{
self::assertThat(self::getResponse(), LogicalAnd::fromConstraints(
self::assertThatForResponse(LogicalAnd::fromConstraints(
new ResponseConstraint\ResponseHasCookie($name, $path, $domain),
new ResponseConstraint\ResponseCookieValueSame($name, $expectedValue, $path, $domain)
), $message);
Expand Down Expand Up @@ -124,6 +126,21 @@ public static function assertRouteSame($expectedRoute, array $parameters = [], s
self::assertThat(self::getRequest(), $constraint, $message);
}

public static function assertThatForResponse(Constraint $constraint, string $message = ''): void
{
try {
self::assertThat(self::getResponse(), $constraint, $message);
} catch (ExpectationFailedException $exception) {
if (($serverExceptionMessage = self::getResponse()->headers->get('X-Debug-Exception'))
&& ($serverExceptionFile = self::getResponse()->headers->get('X-Debug-Exception-File'))) {
$serverExceptionFile = explode(':', $serverExceptionFile);
$exception->__construct($exception->getMessage(), $exception->getComparisonFailure(), new \ErrorException(rawurldecode($serverExceptionMessage), 0, 1, rawurldecode($serverExceptionFile[0]), $serverExceptionFile[1]), $exception->getPrevious());
}

throw $exception;
}
}

private static function getClient(AbstractBrowser $newClient = null): ?AbstractBrowser
{
static $client;
Expand Down
12 changes: 12 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Test;

use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestAssertionsTrait;
Expand Down Expand Up @@ -235,6 +236,17 @@ public function testAssertRouteSame()
$this->getRequestTester()->assertRouteSame('articles');
}

public function testExceptionOnServerError()
{
try {
$this->getResponseTester(new Response('', 500, ['X-Debug-Exception' => 'An exception has occurred', 'X-Debug-Exception-File' => '%2Fsrv%2Ftest.php:12']))->assertResponseIsSuccessful();
} catch (ExpectationFailedException $exception) {
$this->assertSame('An exception has occurred', $exception->getPrevious()->getMessage());
$this->assertSame('/srv/test.php', $exception->getPrevious()->getFile());
$this->assertSame(12, $exception->getPrevious()->getLine());
}
}

private function getResponseTester(Response $response): WebTestCase
{
$client = $this->createMock(KernelBrowser::class);
Expand Down
5 changes: 5 additions & 0 deletions src/Symfony/Component/ErrorHandler/CHANGELOG.md
@@ -1,6 +1,11 @@
CHANGELOG
=========

5.1.0
-----

* The `HtmlErrorRenderer` and `SerializerErrorRenderer` add `X-Debug-Exception` and `X-Debug-Exception-File` headers in debug mode.

4.4.0
-----

Expand Down
Expand Up @@ -66,9 +66,13 @@ public function __construct($debug = false, string $charset = null, $fileLinkFor
*/
public function render(\Throwable $exception): FlattenException
{
$exception = FlattenException::createFromThrowable($exception, null, [
'Content-Type' => 'text/html; charset='.$this->charset,
]);
$headers = ['Content-Type' => 'text/html; charset='.$this->charset];
if (\is_bool($this->debug) ? $this->debug : ($this->debug)($exception)) {
$headers['X-Debug-Exception'] = rawurlencode($exception->getMessage());
$headers['X-Debug-Exception-File'] = rawurlencode($exception->getFile()).':'.$exception->getLine();
}

$exception = FlattenException::createFromThrowable($exception, null, $headers);

return $exception->setAsString($this->renderException($exception));
}
Expand Down
Expand Up @@ -53,14 +53,21 @@ public function __construct(SerializerInterface $serializer, $format, ErrorRende
*/
public function render(\Throwable $exception): FlattenException
{
$flattenException = FlattenException::createFromThrowable($exception);
$headers = [];
$debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception);
if ($debug) {
$headers['X-Debug-Exception'] = rawurlencode($exception->getMessage());
$headers['X-Debug-Exception-File'] = rawurlencode($exception->getFile()).':'.$exception->getLine();
}

$flattenException = FlattenException::createFromThrowable($exception, null, $headers);

try {
$format = \is_string($this->format) ? $this->format : ($this->format)($flattenException);

return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, [
'exception' => $exception,
'debug' => \is_bool($this->debug) ? $this->debug : ($this->debug)($exception),
'debug' => $debug,
]));
} catch (NotEncodableValueException $e) {
return $this->fallbackErrorRenderer->render($exception);
Expand Down

0 comments on commit 693cf11

Please sign in to comment.