From d3a051aaf8d16d52a23adc9ff17340744c66a407 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Wed, 10 Apr 2024 20:15:08 +0100 Subject: [PATCH] [11.x] Introduces `Exceptions` facade (#50704) * Initial work on `Exceptions` facade * Apply fixes from StyleCI * Allows to fake expecific exceptions * Adds `throwOnReport` and `throwReported` * Apply fixes from StyleCI * Fixes reporting of regular errors like 404 or 302 * More tests * More tests * Update src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php Co-authored-by: Tim MacDonald * Update src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php Co-authored-by: Tim MacDonald * Adjusts code * formatting * marker interface * Apply fixes from StyleCI --------- Co-authored-by: StyleCI Bot Co-authored-by: Dries Vints Co-authored-by: Tim MacDonald Co-authored-by: Taylor Otwell --- .../InteractsWithExceptionHandling.php | 24 +- .../WithoutExceptionHandlingHandler.php | 8 + src/Illuminate/Support/Facades/Exceptions.php | 68 ++ .../Testing/Fakes/ExceptionHandlerFake.php | 275 ++++++++ .../Support/ExceptionsFacadeTest.php | 594 ++++++++++++++++++ 5 files changed, 965 insertions(+), 4 deletions(-) create mode 100644 src/Illuminate/Foundation/Testing/Concerns/WithoutExceptionHandlingHandler.php create mode 100644 src/Illuminate/Support/Facades/Exceptions.php create mode 100644 src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php create mode 100644 tests/Integration/Support/ExceptionsFacadeTest.php diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php index 0955682430b8..9c95c137eabc 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithExceptionHandling.php @@ -4,6 +4,8 @@ use Closure; use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Support\Facades\Exceptions; +use Illuminate\Support\Testing\Fakes\ExceptionHandlerFake; use Illuminate\Testing\Assert; use Illuminate\Validation\ValidationException; use Symfony\Component\Console\Application as ConsoleApplication; @@ -27,7 +29,11 @@ trait InteractsWithExceptionHandling protected function withExceptionHandling() { if ($this->originalExceptionHandler) { - $this->app->instance(ExceptionHandler::class, $this->originalExceptionHandler); + $currentExceptionHandler = app(ExceptionHandler::class); + + $currentExceptionHandler instanceof ExceptionHandlerFake + ? $currentExceptionHandler->setHandler($this->originalExceptionHandler) + : $this->app->instance(ExceptionHandler::class, $this->originalExceptionHandler); } return $this; @@ -63,10 +69,14 @@ protected function handleValidationExceptions() protected function withoutExceptionHandling(array $except = []) { if ($this->originalExceptionHandler == null) { - $this->originalExceptionHandler = app(ExceptionHandler::class); + $currentExceptionHandler = app(ExceptionHandler::class); + + $this->originalExceptionHandler = $currentExceptionHandler instanceof ExceptionHandlerFake + ? $currentExceptionHandler->handler() + : $currentExceptionHandler; } - $this->app->instance(ExceptionHandler::class, new class($this->originalExceptionHandler, $except) implements ExceptionHandler + $exceptionHandler = new class($this->originalExceptionHandler, $except) implements ExceptionHandler, WithoutExceptionHandlingHandler { protected $except; protected $originalHandler; @@ -145,7 +155,13 @@ public function renderForConsole($output, Throwable $e) { (new ConsoleApplication)->renderThrowable($e, $output); } - }); + }; + + $currentExceptionHandler = app(ExceptionHandler::class); + + $currentExceptionHandler instanceof ExceptionHandlerFake + ? $currentExceptionHandler->setHandler($exceptionHandler) + : $this->app->instance(ExceptionHandler::class, $exceptionHandler); return $this; } diff --git a/src/Illuminate/Foundation/Testing/Concerns/WithoutExceptionHandlingHandler.php b/src/Illuminate/Foundation/Testing/Concerns/WithoutExceptionHandlingHandler.php new file mode 100644 index 000000000000..07cda4f5950d --- /dev/null +++ b/src/Illuminate/Foundation/Testing/Concerns/WithoutExceptionHandlingHandler.php @@ -0,0 +1,8 @@ +>|class-string<\Throwable> $exceptions + * @return \Illuminate\Support\Testing\Fakes\ExceptionHandlerFake + */ + public static function fake(array|string $exceptions = []) + { + $exceptionHandler = static::isFake() + ? static::getFacadeRoot()->handler() + : static::getFacadeRoot(); + + return tap(new ExceptionHandlerFake($exceptionHandler, Arr::wrap($exceptions)), function ($fake) { + static::swap($fake); + }); + } + + /** + * Get the registered name of the component. + * + * @return string + */ + protected static function getFacadeAccessor() + { + return ExceptionHandler::class; + } +} diff --git a/src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php b/src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php new file mode 100644 index 000000000000..b2763daf88d0 --- /dev/null +++ b/src/Illuminate/Support/Testing/Fakes/ExceptionHandlerFake.php @@ -0,0 +1,275 @@ + + */ + protected $reported = []; + + /** + * If the fake should throw exceptions when they are reported. + * + * @var bool + */ + protected $throwOnReport = false; + + /** + * Create a new exception handler fake. + * + * @param \Illuminate\Contracts\Debug\ExceptionHandler $handler + * @param array> $exceptions + * @return void + */ + public function __construct( + protected ExceptionHandler $handler, + protected array $exceptions = [], + ) { + // + } + + /** + * Get the underlying handler implementation. + * + * @return \Illuminate\Contracts\Debug\ExceptionHandler + */ + public function handler() + { + return $this->handler; + } + + /** + * Assert if an exception of the given type has been reported. + * + * @param \Closure|string $exception + * @return void + */ + public function assertReported(Closure|string $exception) + { + $message = sprintf( + 'The expected [%s] exception was not reported.', + is_string($exception) ? $exception : $this->firstClosureParameterType($exception) + ); + + if (is_string($exception)) { + Assert::assertTrue( + in_array($exception, array_map('get_class', $this->reported), true), + $message, + ); + + return; + } + + Assert::assertTrue( + collect($this->reported)->contains( + fn (Throwable $e) => $this->firstClosureParameterType($exception) === get_class($e) + && $exception($e) === true, + ), $message, + ); + } + + /** + * Assert the number of exceptions that have been reported. + * + * @param int $count + * @return void + */ + public function assertReportedCount(int $count) + { + $total = collect($this->reported)->count(); + + PHPUnit::assertSame( + $count, $total, + "The total number of exceptions reported was {$total} instead of {$count}." + ); + } + + /** + * Assert if an exception of the given type has not been reported. + * + * @param \Closure|string $exception + * @return void + */ + public function assertNotReported(Closure|string $exception) + { + try { + $this->assertReported($exception); + } catch (ExpectationFailedException $e) { + return; + } + + throw new ExpectationFailedException(sprintf( + 'The expected [%s] exception was not reported.', + is_string($exception) ? $exception : $this->firstClosureParameterType($exception) + )); + } + + /** + * Assert nothing has been reported. + * + * @return void + */ + public function assertNothingReported() + { + Assert::assertEmpty( + $this->reported, + sprintf( + 'The following exceptions were reported: %s.', + implode(', ', array_map('get_class', $this->reported)), + ), + ); + } + + /** + * Report or log an exception. + * + * @param \Throwable $e + * @return void + */ + public function report($e) + { + if (! $this->isFakedException($e)) { + $this->handler->report($e); + + return; + } + + if (! $this->shouldReport($e)) { + return; + } + + $this->reported[] = $e; + + if ($this->throwOnReport) { + throw $e; + } + } + + /** + * Determine if the given exception is faked. + * + * @param \Throwable $e + * @return bool + */ + protected function isFakedException(Throwable $e) + { + return count($this->exceptions) === 0 || in_array(get_class($e), $this->exceptions, true); + } + + /** + * Determine if the exception should be reported. + * + * @param \Throwable $e + * @return bool + */ + public function shouldReport($e) + { + return $this->runningWithoutExceptionHandling() || $this->handler->shouldReport($e); + } + + /** + * Determine if the handler is running without exception handling. + * + * @return bool + */ + protected function runningWithoutExceptionHandling() + { + return $this->handler instanceof WithoutExceptionHandlingHandler; + } + + /** + * Render an exception into an HTTP response. + * + * @param \Illuminate\Http\Request $request + * @param \Throwable $e + * @return \Symfony\Component\HttpFoundation\Response + */ + public function render($request, $e) + { + return $this->handler->render($request, $e); + } + + /** + * Render an exception to the console. + * + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @param \Throwable $e + * @return void + */ + public function renderForConsole($output, Throwable $e) + { + $this->handler->renderForConsole($output, $e); + } + + /** + * Throw exceptions when they are reported. + * + * @return $this + */ + public function throwOnReport() + { + $this->throwOnReport = true; + + return $this; + } + + /** + * Throw the first reported exception. + * + * @return $this + * + * @throws \Throwable + */ + public function throwFirstReported() + { + foreach ($this->reported as $e) { + throw $e; + } + + return $this; + } + + /** + * Set the "original" handler that should be used by the fake. + * + * @param \Illuminate\Contracts\Debug\ExceptionHandler $handler + * @return $this + */ + public function setHandler(ExceptionHandler $handler) + { + $this->handler = $handler; + + return $this; + } + + /** + * Handle dynamic method calls to the mailer. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call(string $method, array $parameters) + { + return $this->forwardCallTo($this->handler, $method, $parameters); + } +} diff --git a/tests/Integration/Support/ExceptionsFacadeTest.php b/tests/Integration/Support/ExceptionsFacadeTest.php new file mode 100644 index 000000000000..22d1b7bc8e98 --- /dev/null +++ b/tests/Integration/Support/ExceptionsFacadeTest.php @@ -0,0 +1,594 @@ + $e->getMessage() === 'test 1'); + Exceptions::assertReported(fn (RuntimeException $e) => $e->getMessage() === 'test 2'); + Exceptions::assertReportedCount(2); + } + + public function testFakeAssertReportedCount() + { + Exceptions::fake(); + + Exceptions::report(new RuntimeException('test 1')); + report(new RuntimeException('test 2')); + + Exceptions::assertReportedCount(2); + } + + public function testFakeAssertReportedCountMayFail() + { + Exceptions::fake(); + + Exceptions::report(new RuntimeException('test 1')); + report(new RuntimeException('test 2')); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('The total number of exceptions reported was 2 instead of 1.'); + + Exceptions::assertReportedCount(1); + } + + public function testFakeAssertReportedWithFakedExceptions() + { + Exceptions::fake([ + RuntimeException::class, + ]); + + Exceptions::report(new RuntimeException('test 1')); + report(new RuntimeException('test 2')); + report(new InvalidArgumentException('test 3')); + + Exceptions::assertReported(RuntimeException::class); + Exceptions::assertReported(fn (RuntimeException $e) => $e->getMessage() === 'test 1'); + Exceptions::assertReported(fn (RuntimeException $e) => $e->getMessage() === 'test 2'); + + Exceptions::assertNotReported(InvalidArgumentException::class); + Exceptions::assertReportedCount(2); + } + + public function testFakeAssertReportedAsStringMayFail() + { + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('The expected [InvalidArgumentException] exception was not reported.'); + + Exceptions::fake(); + + Exceptions::report(new RuntimeException('test 1')); + + Exceptions::assertReportedCount(1); + Exceptions::assertReported(InvalidArgumentException::class); + } + + public function testFakeAssertReportedAsClosureMayFail() + { + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('The expected [InvalidArgumentException] exception was not reported.'); + + Exceptions::fake(); + + Exceptions::report(new RuntimeException('test 1')); + + Exceptions::assertReportedCount(1); + Exceptions::assertReported(fn (InvalidArgumentException $e) => $e->getMessage() === 'test 2'); + } + + public function testFakeAssertReportedWithFakedExceptionsMayFail() + { + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('The expected [RuntimeException] exception was not reported.'); + + Exceptions::fake(InvalidArgumentException::class); + + Exceptions::report(new InvalidArgumentException('test 1')); + report(new RuntimeException('test 2')); + + Exceptions::assertReported(InvalidArgumentException::class); + Exceptions::assertReported(RuntimeException::class); + } + + public function testFakeAssertNotReported() + { + Exceptions::fake(); + + Exceptions::report(new RuntimeException('test 1')); + report(new RuntimeException('test 2')); + + Exceptions::assertNotReported(InvalidArgumentException::class); + Exceptions::assertNotReported(fn (InvalidArgumentException $e) => $e->getMessage() === 'test 1'); + Exceptions::assertNotReported(fn (InvalidArgumentException $e) => $e->getMessage() === 'test 2'); + Exceptions::assertNotReported(fn (InvalidArgumentException $e) => $e->getMessage() === 'test 3'); + Exceptions::assertNotReported(fn (InvalidArgumentException $e) => $e->getMessage() === 'test 4'); + + Exceptions::assertReportedCount(2); + } + + public function testFakeAssertNotReportedWithFakedExceptions() + { + Exceptions::fake([ + InvalidArgumentException::class, + ]); + + report(new RuntimeException('test 2')); + + Exceptions::assertNotReported(InvalidArgumentException::class); + Exceptions::assertNotReported(RuntimeException::class); + } + + public function testFakeAssertNotReportedMayFail() + { + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('The expected [RuntimeException] exception was not reported.'); + + Exceptions::fake(); + + Exceptions::report(new RuntimeException('test 1')); + + Exceptions::assertNotReported(RuntimeException::class); + } + + public function testFakeAssertNotReportedAsClosureMayFail() + { + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('The expected [RuntimeException] exception was not reported.'); + + Exceptions::fake(); + + Exceptions::report(new RuntimeException('test 1')); + + Exceptions::assertNotReported(fn (RuntimeException $e) => $e->getMessage() === 'test 1'); + } + + public function testResolvesExceptionHandler() + { + $this->assertInstanceOf( + ExceptionHandler::class, + Exceptions::getFacadeRoot() + ); + } + + public function testFakeAssertNothingReported() + { + Exceptions::fake(); + + Exceptions::assertNothingReported(); + } + + public function testFakeAssertNothingReportedWithFakedExceptions() + { + Exceptions::fake([ + InvalidArgumentException::class, + ]); + + report(new RuntimeException('test 1')); + + Exceptions::assertNothingReported(); + } + + public function testFakeAssertNothingReportedMayFail() + { + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('The following exceptions were reported: RuntimeException, RuntimeException, InvalidArgumentException.'); + + Exceptions::fake(); + + Exceptions::report(new RuntimeException('test 1')); + report(new RuntimeException('test 2')); + report(new InvalidArgumentException('test 3')); + + Exceptions::assertNothingReported(); + } + + public function testFakeMethodReturnsExceptionHandlerFake() + { + $this->assertInstanceOf(ExceptionHandlerFake::class, $fake = Exceptions::fake()); + $this->assertInstanceOf(ExceptionHandlerFake::class, Exceptions::getFacadeRoot()); + $this->assertInstanceOf(Handler::class, $fake->handler()); + + $this->assertInstanceOf(ExceptionHandlerFake::class, $fake = Exceptions::fake()); + $this->assertInstanceOf(ExceptionHandlerFake::class, Exceptions::getFacadeRoot()); + $this->assertInstanceOf(Handler::class, $fake->handler()); + } + + public function testReportedExceptionsAreNotThrownByDefault() + { + report(new Exception('Test exception')); + + $this->assertTrue(true); + } + + public function testReportedExceptionsAreNotThrownByDefaultWithExceptionHandling() + { + Route::get('/', function () { + report(new Exception('Test exception')); + }); + + $this->get('/')->assertStatus(200); + } + + public function testReportedExceptionsAreNotThrownByDefaultWithoutExceptionHandling() + { + $this->withoutExceptionHandling(); + + Route::get('/', function () { + report(new Exception('Test exception')); + }); + + $this->get('/')->assertStatus(200); + } + + public function testThrowOnReport() + { + Exceptions::fake()->throwOnReport(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Test exception'); + + report(new Exception('Test exception')); + } + + public function testThrowOnReportDoesNotThrowExceptionsThatShouldNotBeReported() + { + Exceptions::fake()->throwOnReport(); + + Route::get('/302', function () { + Validator::validate(['name' => ''], ['name' => 'required']); + }); + + $this->get('/302')->assertStatus(302); + + Route::get('/404', function () { + throw new ModelNotFoundException(); + }); + + $this->get('/404')->assertStatus(404); + + $this->doesNotPerformAssertions(); + + Exceptions::assertReportedCount(0); + } + + public function testThrowOnReportWithExceptionHandling() + { + Exceptions::fake()->throwOnReport(); + + Route::get('/', function () { + report(new Exception('Test exception')); + }); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Test exception'); + + $this->get('/'); + } + + public function testThrowOnReportWithoutExceptionHandling() + { + Exceptions::fake()->throwOnReport(); + + $this->withoutExceptionHandling(); + + Route::get('/', function () { + report(new Exception('Test exception')); + }); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Test exception'); + + $this->get('/'); + } + + public function testThrowOnReportRegardlessOfTheCallingOrderOfWithoutExceptionHandling() + { + Exceptions::fake()->throwOnReport(); + + $this + ->withoutExceptionHandling() + ->withExceptionHandling() + ->withoutExceptionHandling(); + + Route::get('/', function () { + rescue(fn () => throw new Exception('Test exception')); + }); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Test exception'); + + $this->get('/'); + } + + public function testThrowOnReportRegardlessOfTheCallingOrderOfWithExceptionHandling() + { + Exceptions::fake()->throwOnReport(); + + $this->withoutExceptionHandling() + ->withExceptionHandling() + ->withoutExceptionHandling() + ->withExceptionHandling(); + + Route::get('/', function () { + rescue(fn () => throw new Exception('Test exception')); + }); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Test exception'); + + $this->get('/'); + } + + public function testThrowOnReportWithFakedExceptions() + { + Exceptions::fake([InvalidArgumentException::class])->throwOnReport(); + + $this->expectException(InvalidArgumentException::class); + + report(new Exception('Test exception')); + report(new RuntimeException('Test exception')); + report(new InvalidArgumentException('Test exception')); + } + + public function testThrowOnReportWithFakedExceptionsFromFacade() + { + Exceptions::fake([InvalidArgumentException::class])->throwOnReport(); + + $this->expectException(InvalidArgumentException::class); + + report(new Exception('Test exception')); + report(new RuntimeException('Test exception')); + Exceptions::assertReportedCount(0); + + report(new InvalidArgumentException('Test exception')); + } + + public function testThrowOnReporEvenWhenAppReportablesReturnFalse() + { + app(ExceptionHandler::class)->reportable(function (Throwable $e) { + return false; + }); + + Exceptions::fake()->throwOnReport(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Test exception'); + + report(new Exception('Test exception')); + } + + public function testAppReportablesAreNotCalledIfExceptionIsNotFaked() + { + app(ExceptionHandler::class)->reportable(function (Throwable $e) { + throw new InvalidArgumentException($e->getMessage()); + }); + + Exceptions::fake([RuntimeException::class, Exception::class]); + + report(new Exception('My exception message')); + + Exceptions::assertReported(Exception::class); + } + + public function testThrowOnReportLeaveAppReportablesUntouched() + { + app(ExceptionHandler::class)->reportable(function (Throwable $e) { + throw new InvalidArgumentException($e->getMessage()); + }); + + Exceptions::fake([RuntimeException::class])->throwOnReport(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('My exception message'); + + report(new Exception('My exception message')); + } + + public function testThrowReportedExceptions() + { + Exceptions::fake(); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Test exception'); + + report(new Exception('Test exception')); + + Exceptions::throwFirstReported(); + } + + public function testThrowReportedExceptionsWithFakedExceptions() + { + Exceptions::fake([InvalidArgumentException::class]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Test exception'); + + report(new RuntimeException('Test exception')); + report(new InvalidArgumentException('Test exception')); + + Exceptions::throwFirstReported(); + } + + public function testThrowReportedExceptionsWhenThereIsNone() + { + Exceptions::fake(); + + Exceptions::throwFirstReported(); + + Exceptions::fake([InvalidArgumentException::class]); + + report(new RuntimeException('Test exception')); + + Exceptions::throwFirstReported(); + + $this->doesNotPerformAssertions(); + } + + public function testFakingExceptionsThatShouldNotBeReportedWithExceptionHandling() + { + Exceptions::fake(); + + Route::get('/302', function () { + Validator::validate(['name' => ''], ['name' => 'required']); + }); + + $this->get('/302')->assertStatus(302); + + Route::get('/404', function () { + throw new ModelNotFoundException(); + }); + + $this->get('/404')->assertStatus(404); + + report(new ModelNotFoundException()); + + Exceptions::assertNothingReported(); + } + + public function testFakingExceptionsThatShouldNotBeReportedWithRescueAndWithoutExceptionHandling() + { + Exceptions::fake(); + + $this->withoutExceptionHandling(); + + Route::get('/validation', function () { + rescue(fn () => Validator::validate(['name' => ''], ['name' => 'required'])); + }); + + $this->get('/validation')->assertStatus(200); + + Route::get('/model', function () { + rescue(fn () => throw new ModelNotFoundException()); + }); + + $this->get('/model')->assertStatus(200); + + rescue(fn () => throw new ModelNotFoundException()); + + Exceptions::assertReportedCount(3); + } + + public function testRescue() + { + Exceptions::fake(); + + rescue(fn () => throw new Exception('Test exception')); + + Exceptions::assertReported(Exception::class); + } + + public function testRescueWithoutReport() + { + Exceptions::fake(); + + rescue(fn () => throw new Exception('Test exception'), null, false); + + Exceptions::assertNothingReported(); + } + + public function testFlowBetweenFakeAndTestExceptionHandling() + { + $this->assertInstanceOf(Handler::class, app(ExceptionHandler::class)); + + Exceptions::fake(); + $this->assertInstanceOf(ExceptionHandlerFake::class, app(ExceptionHandler::class)); + $this->assertInstanceOf(Handler::class, Exceptions::fake()->handler()); + $this->assertFalse((new \ReflectionClass(Exceptions::fake()->handler()))->isAnonymous()); + + Exceptions::fake(); + $this->assertInstanceOf(ExceptionHandlerFake::class, app(ExceptionHandler::class)); + $this->assertInstanceOf(Handler::class, Exceptions::fake()->handler()); + $this->assertFalse((new \ReflectionClass(Exceptions::fake()->handler()))->isAnonymous()); + + $this->withoutExceptionHandling(); + $this->assertInstanceOf(ExceptionHandlerFake::class, app(ExceptionHandler::class)); + $this->assertInstanceOf(ExceptionHandler::class, Exceptions::fake()->handler()); + $this->assertTrue((new \ReflectionClass(Exceptions::fake()->handler()))->isAnonymous()); + + $this->withExceptionHandling(); + $this->assertInstanceOf(ExceptionHandlerFake::class, app(ExceptionHandler::class)); + $this->assertInstanceOf(ExceptionHandler::class, Exceptions::fake()->handler()); + $this->assertFalse((new \ReflectionClass(Exceptions::fake()->handler()))->isAnonymous()); + + Exceptions::fake(); + $this->assertInstanceOf(ExceptionHandlerFake::class, app(ExceptionHandler::class)); + $this->assertInstanceOf(Handler::class, Exceptions::fake()->handler()); + $this->assertFalse((new \ReflectionClass(Exceptions::fake()->handler()))->isAnonymous()); + } + + public function testFlowBetweenTestExceptionHandlingAndFake() + { + $this->withoutExceptionHandling(); + $this->assertTrue((new \ReflectionClass(app(ExceptionHandler::class)))->isAnonymous()); + + Exceptions::fake(); + $this->assertInstanceOf(ExceptionHandlerFake::class, app(ExceptionHandler::class)); + $this->assertInstanceOf(ExceptionHandler::class, Exceptions::fake()->handler()); + $this->assertTrue((new \ReflectionClass(Exceptions::fake()->handler()))->isAnonymous()); + + Exceptions::fake(); + $this->assertInstanceOf(ExceptionHandlerFake::class, app(ExceptionHandler::class)); + $this->assertInstanceOf(ExceptionHandler::class, Exceptions::fake()->handler()); + $this->assertTrue((new \ReflectionClass(Exceptions::fake()->handler()))->isAnonymous()); + + $this->withExceptionHandling(); + $this->assertInstanceOf(ExceptionHandlerFake::class, app(ExceptionHandler::class)); + $this->assertInstanceOf(Handler::class, Exceptions::fake()->handler()); + $this->assertFalse((new \ReflectionClass(Exceptions::fake()->handler()))->isAnonymous()); + } + + public function testWithDeprecationHandling() + { + Exceptions::fake(); + + Route::get('/', function () { + str_contains(null, null); + }); + + $this->get('/')->assertStatus(200); + + Exceptions::assertNothingReported(); + } + + public function testWithoutDeprecationHandler() + { + Exceptions::fake(); + + $this->withoutDeprecationHandling(); + + Route::get('/', function () { + str_contains(null, null); + }); + + $this->get('/')->assertStatus(500); + + Exceptions::assertReported(function (ErrorException $e) { + return $e->getMessage() === 'str_contains(): Passing null to parameter #1 ($haystack) of type string is deprecated'; + }); + + Exceptions::assertReportedCount(1); + } +}