feature #36003 [ErrorHandler][FrameworkBundle] better error messages 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
-------
0da9469ee2
[ErrorHandler][FrameworkBundle] better error messages in failing tests
This commit is contained in:
commit
693cf11b8e
@ -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
|
||||
-----
|
||||
|
@ -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;
|
||||
@ -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
|
||||
@ -46,42 +48,42 @@ trait BrowserKitAssertionsTrait
|
||||
$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);
|
||||
@ -124,6 +126,21 @@ trait BrowserKitAssertionsTrait
|
||||
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;
|
||||
|
@ -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;
|
||||
@ -235,6 +236,17 @@ class WebTestCaseTest extends TestCase
|
||||
$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);
|
||||
|
@ -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
|
||||
-----
|
||||
|
||||
|
@ -66,9 +66,13 @@ class HtmlErrorRenderer implements ErrorRendererInterface
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
@ -53,14 +53,21 @@ class SerializerErrorRenderer implements ErrorRendererInterface
|
||||
*/
|
||||
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);
|
||||
|
Reference in New Issue
Block a user