From 6a268471f23525ead824e0a12a94c558c4cdd13b Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Fri, 1 Apr 2022 14:21:17 -0400 Subject: [PATCH] [feature] add `TestCommand::expectException()` --- README.md | 14 +++++++++++ src/TestCommand.php | 39 ++++++++++++++++++++++++++++- tests/FunctionalTest.php | 54 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 13b4c02..c362700 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,13 @@ class CreateUserCommandTest extends KernelTestCase ->assertOutputContains('Creating regular user "kbond"') ; + // test command throws exception + $this->consoleCommand(CreateUserCommand::class) + ->expectException(\RuntimeException::class, 'Username required!') + ->assertStatusCode(1) + ->assertOutputContains('Could not create user!') // can still make assertions on output before exception was thrown + ; + // access result $result = $this->executeConsoleCommand('create:user'); @@ -130,6 +137,13 @@ class CreateUserCommandTest extends TestCase ->assertOutputContains('Creating regular user "kbond"') ; + // test command throws exception + TestCommand::for(new CreateUserCommand(/** args... */)) + ->expectException(\RuntimeException::class, 'Username required!') + ->assertStatusCode(1) + ->assertOutputContains('Could not create user!') // can still make assertions on output before exception was thrown + ; + // access result $result = TestCommand::for(new CreateUserCommand(/** args... */))->execute(); diff --git a/src/TestCommand.php b/src/TestCommand.php index 7b7e07e..8f84a9c 100644 --- a/src/TestCommand.php +++ b/src/TestCommand.php @@ -4,6 +4,7 @@ use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; +use Zenstruck\Assert; /** * @author Kevin Bond @@ -15,6 +16,10 @@ final class TestCommand /** @var string[] */ private array $inputs = []; + + /** @var callable|class-string|null */ + private $expectedException; + private ?string $expectedExceptionMessage = null; private bool $splitOutputStreams = false; private function __construct(Command $command, string $cli) @@ -96,6 +101,25 @@ public function withInput(array $inputs): self return $this; } + /** + * Expect executing the command will throw this exception. Fails if not thrown. + * + * @param class-string|callable $expectedException string: class name of the expected exception + * callable: uses the first argument's type-hint + * to determine the expected exception class. When + * exception is caught, callable is invoked with + * the caught exception + * @param string|null $expectedMessage Assert the caught exception message "contains" + * this string + */ + public function expectException($expectedException, ?string $expectedMessage = null): self + { + $this->expectedException = $expectedException; + $this->expectedExceptionMessage = $expectedMessage; + + return $this; + } + public function execute(?string $cli = null): CommandResult { $autoExit = $this->application->isAutoExitEnabled(); @@ -105,7 +129,7 @@ public function execute(?string $cli = null): CommandResult $this->application->setAutoExit(false); $this->application->setCatchExceptions(false); - $status = $this->application->run( + $status = $this->doRun( $input = new TestInput($cli, $this->inputs), $output = new TestOutput($this->splitOutputStreams, $input) ); @@ -115,4 +139,17 @@ public function execute(?string $cli = null): CommandResult return new CommandResult($cli, $status, $output); } + + private function doRun(TestInput $input, TestOutput $output): int + { + $fn = fn() => $this->application->run($input, $output); + + if (!$this->expectedException) { + return $fn(); + } + + Assert::that($fn)->throws($this->expectedException, $this->expectedExceptionMessage); + + return 1; + } } diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index d4276c1..3f0ae89 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -2,8 +2,10 @@ namespace Zenstruck\Console\Test\Tests; +use PHPUnit\Framework\AssertionFailedError; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Exception\CommandNotFoundException; +use Zenstruck\Assert; use Zenstruck\Console\Test\InteractsWithConsole; use Zenstruck\Console\Test\Tests\Fixture\FixtureCommand; @@ -136,6 +138,58 @@ public function exceptions_from_commands_are_thrown(): void $this->consoleCommand('fixture:command --throw')->execute(); } + /** + * @test + */ + public function can_expect_exception(): void + { + $this->consoleCommand('fixture:command --throw') + ->expectException(\RuntimeException::class) + ->execute() + ->assertStatusCode(1) + ->assertOutputContains('Executing command...') + ->assertOutputContains('Error output.') + ; + + $this->consoleCommand('fixture:command --throw') + ->expectException(\RuntimeException::class, 'Exception thrown!') + ->execute() + ->assertStatusCode(1) + ->assertOutputContains('Executing command...') + ->assertOutputContains('Error output.') + ; + + $this->consoleCommand('fixture:command --throw') + ->expectException(function(\RuntimeException $e) { + $this->assertSame('Exception thrown!', $e->getMessage()); + }) + ->execute() + ->assertStatusCode(1) + ->assertOutputContains('Executing command...') + ->assertOutputContains('Error output.') + ; + } + + /** + * @test + */ + public function if_expected_exception_not_thrown_fail(): void + { + Assert::that(function() { + $this->consoleCommand('fixture:command') + ->expectException(\RuntimeException::class) + ->execute() + ; + })->throws(AssertionFailedError::class, 'No exception thrown. Expected "RuntimeException".'); + + Assert::that(function() { + $this->consoleCommand('fixture:command --throw') + ->expectException(\LogicException::class) + ->execute() + ; + })->throws(AssertionFailedError::class, 'Expected "LogicException" to be thrown but got "RuntimeException".'); + } + /** * @test */