diff --git a/composer.json b/composer.json index 0b7f92b..002a6c7 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "albertcht/lumen-testing", + "name": "unicorn/lumen-testing", "description": "Testing Suite For Lumen like Laravel does.", "keywords": ["phpunit", "test", "lumen", "testing", "laravel", "laravel"], "license": "MIT", @@ -10,7 +10,6 @@ "require": { "php": "^7.1", "laravel/lumen-framework": "~5.3|~6.0" - }, "require-dev": { "phpunit/phpunit": "^7.0", diff --git a/src/Concerns/InteractsWithContainer.php b/src/Concerns/InteractsWithContainer.php index 75e2a83..27d62f0 100644 --- a/src/Concerns/InteractsWithContainer.php +++ b/src/Concerns/InteractsWithContainer.php @@ -2,6 +2,9 @@ namespace AlbertCht\Lumen\Testing\Concerns; +use Closure; +use Mockery; + trait InteractsWithContainer { /** @@ -27,4 +30,27 @@ protected function instance($abstract, $instance) $this->app->instance($abstract, $instance); return $instance; } + + /** + * Mock an instance of an object in the container. + * + * @param string $abstract + * @param \Closure|null $mock + * @return object + */ + protected function mock($abstract, Closure $mock = null) + { + return $this->instance($abstract, Mockery::mock(...array_filter(func_get_args()))); + } + /** + * Spy an instance of an object in the container. + * + * @param string $abstract + * @param \Closure|null $mock + * @return object + */ + protected function spy($abstract, Closure $mock = null) + { + return $this->instance($abstract, Mockery::spy(...array_filter(func_get_args()))); + } } diff --git a/src/Concerns/InteractsWithExceptionHandling.php b/src/Concerns/InteractsWithExceptionHandling.php index e5a1687..128cf41 100644 --- a/src/Concerns/InteractsWithExceptionHandling.php +++ b/src/Concerns/InteractsWithExceptionHandling.php @@ -25,7 +25,8 @@ trait InteractsWithExceptionHandling protected function withExceptionHandling() { if ($this->originalExceptionHandler) { - $this->app->instance(ExceptionHandler::class, $this->originalExceptionHandler); + $this->app->instance(ExceptionHandler::class, + $this->originalExceptionHandler); } return $this; @@ -35,6 +36,7 @@ protected function withExceptionHandling() * Only handle the given exceptions via the exception handler. * * @param array $exceptions + * * @return $this */ protected function handleExceptions(array $exceptions) @@ -56,6 +58,7 @@ protected function handleValidationExceptions() * Disable exception handling for the test. * * @param array $except + * * @return $this */ protected function withoutExceptionHandling(array $except = []) @@ -64,72 +67,90 @@ protected function withoutExceptionHandling(array $except = []) $this->originalExceptionHandler = app(ExceptionHandler::class); } - $this->app->instance(ExceptionHandler::class, new class($this->originalExceptionHandler, $except) implements ExceptionHandler { - protected $except; - protected $originalHandler; - - /** - * Create a new class instance. - * - * @param \Illuminate\Contracts\Debug\ExceptionHandler - * @param array $except - * @return void - */ - public function __construct($originalHandler, $except = []) - { - $this->except = $except; - $this->originalHandler = $originalHandler; - } - - /** - * Report the given exception. - * - * @param \Exception $e - * @return void - */ - public function report(Exception $e) - { - // - } - - /** - * Render the given exception. - * - * @param \Illuminate\Http\Request $request - * @param \Exception $e - * @return mixed - * - * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException|\Exception - */ - public function render($request, Exception $e) + $this->app->instance(ExceptionHandler::class, + new class($this->originalExceptionHandler, $except) implements + ExceptionHandler { - if ($e instanceof NotFoundHttpException) { - throw new NotFoundHttpException( - "{$request->method()} {$request->url()}", null, $e->getCode() - ); + protected $except; + protected $originalHandler; + + /** + * Create a new class instance. + * + * @param \Illuminate\Contracts\Debug\ExceptionHandler + * @param array $except + * + * @return void + */ + public function __construct($originalHandler, $except = []) + { + $this->except = $except; + $this->originalHandler = $originalHandler; } - foreach ($this->except as $class) { - if ($e instanceof $class) { - return $this->originalHandler->render($request, $e); + /** + * Report the given exception. + * + * @param \Exception $e + * + * @return void + */ + public function report(Exception $e) + { + // + } + + /** + * Render the given exception. + * + * @param \Illuminate\Http\Request $request + * @param \Exception $e + * + * @return mixed + * + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException|\Exception + */ + public function render($request, Exception $e) + { + if ($e instanceof NotFoundHttpException) { + throw new NotFoundHttpException("{$request->method()} {$request->url()}", + null, $e->getCode()); } + + foreach ($this->except as $class) { + if ($e instanceof $class) { + return $this->originalHandler->render($request, $e); + } + } + + throw $e; } - throw $e; - } - - /** - * Render the exception for the console. - * - * @param \Symfony\Component\Console\Output\OutputInterface - * @param \Exception $e - * @return void - */ - public function renderForConsole($output, Exception $e) - { - (new ConsoleApplication)->renderException($e, $output); - } - }); + /** + * Render the exception for the console. + * + * @param \Symfony\Component\Console\Output\OutputInterface + * @param \Exception $e + * + * @return void + */ + public function renderForConsole($output, Exception $e) + { + (new ConsoleApplication)->renderException($e, $output); + } + + /** + * Determine if the exception should be reported. + * + * @param \Exception $e + * + * @return bool + */ + public function shouldReport(Exception $e) + { + return false; + } + }); return $this; } diff --git a/src/PendingCommand.php b/src/PendingCommand.php new file mode 100644 index 0000000..f79e778 --- /dev/null +++ b/src/PendingCommand.php @@ -0,0 +1,194 @@ +app = $app; + $this->test = $test; + $this->command = $command; + $this->parameters = $parameters; + } + /** + * Specify a question that should be asked when the command runs. + * + * @param string $question + * @param string $answer + * @return $this + */ + public function expectsQuestion($question, $answer) + { + $this->test->expectedQuestions[] = [$question, $answer]; + return $this; + } + /** + * Specify output that should be printed when the command runs. + * + * @param string $output + * @return $this + */ + public function expectsOutput($output) + { + $this->test->expectedOutput[] = $output; + return $this; + } + /** + * Assert that the command has the given exit code. + * + * @param int $exitCode + * @return $this + */ + public function assertExitCode($exitCode) + { + $this->expectedExitCode = $exitCode; + return $this; + } + /** + * Execute the command. + * + * @return int + */ + public function execute() + { + return $this->run(); + } + /** + * Execute the command. + * + * @return int + */ + public function run() + { + $this->hasExecuted = true; + $this->mockConsoleOutput(); + try { + $exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters); + } catch (NoMatchingExpectationException $e) { + if ($e->getMethodName() === 'askQuestion') { + $this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.'); + } + throw $e; + } + if ($this->expectedExitCode !== null) { + $this->test->assertEquals( + $this->expectedExitCode, $exitCode, + "Expected status code {$this->expectedExitCode} but received {$exitCode}." + ); + } + return $exitCode; + } + /** + * Mock the application's console output. + * + * @return void + */ + protected function mockConsoleOutput() + { + $mock = Mockery::mock(OutputStyle::class.'[askQuestion]', [ + (new ArrayInput($this->parameters)), $this->createABufferedOutputMock(), + ]); + foreach ($this->test->expectedQuestions as $i => $question) { + $mock->shouldReceive('askQuestion') + ->once() + ->ordered() + ->with(Mockery::on(function ($argument) use ($question) { + return $argument->getQuestion() == $question[0]; + })) + ->andReturnUsing(function () use ($question, $i) { + unset($this->test->expectedQuestions[$i]); + return $question[1]; + }); + } + $this->app->bind(OutputStyle::class, function () use ($mock) { + return $mock; + }); + } + /** + * Create a mock for the buffered output. + * + * @return \Mockery\MockInterface + */ + private function createABufferedOutputMock() + { + $mock = Mockery::mock(BufferedOutput::class.'[doWrite]') + ->shouldAllowMockingProtectedMethods() + ->shouldIgnoreMissing(); + foreach ($this->test->expectedOutput as $i => $output) { + $mock->shouldReceive('doWrite') + ->once() + ->ordered() + ->with($output, Mockery::any()) + ->andReturnUsing(function () use ($i) { + unset($this->test->expectedOutput[$i]); + }); + } + return $mock; + } + /** + * Handle the object's destruction. + * + * @return void + */ + public function __destruct() + { + if ($this->hasExecuted) { + return; + } + $this->run(); + } +}