Skip to content

Commit f6ade8d

Browse files
authored
Merge pull request #10 from kbond/expect-exception
Add `TestCommand::expectException()`
2 parents dad0fae + 991f765 commit f6ade8d

File tree

4 files changed

+106
-12
lines changed

4 files changed

+106
-12
lines changed

.scrutinizer.yml

Lines changed: 0 additions & 10 deletions
This file was deleted.

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# zenstruck/console-test
22

33
[![CI Status](https://github.com/zenstruck/console-test/workflows/CI/badge.svg)](https://github.com/zenstruck/console-test/actions?query=workflow%3ACI)
4-
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/zenstruck/console-test/badges/quality-score.png?b=1.x)](https://scrutinizer-ci.com/g/zenstruck/console-test/?branch=1.x)
54
[![codecov](https://codecov.io/gh/zenstruck/console-test/branch/1.x/graph/badge.svg?token=KPQNKYGYRR)](https://codecov.io/gh/zenstruck/console-test)
65

76
Alternative, opinionated helper for testing Symfony console commands. This package is an alternative to
@@ -71,6 +70,13 @@ class CreateUserCommandTest extends KernelTestCase
7170
->assertOutputContains('Creating regular user "kbond"')
7271
;
7372

73+
// test command throws exception
74+
$this->consoleCommand(CreateUserCommand::class)
75+
->expectException(\RuntimeException::class, 'Username required!')
76+
->assertStatusCode(1)
77+
->assertOutputContains('Could not create user!') // can still make assertions on output before exception was thrown
78+
;
79+
7480
// access result
7581
$result = $this->executeConsoleCommand('create:user');
7682

@@ -130,6 +136,13 @@ class CreateUserCommandTest extends TestCase
130136
->assertOutputContains('Creating regular user "kbond"')
131137
;
132138

139+
// test command throws exception
140+
TestCommand::for(new CreateUserCommand(/** args... */))
141+
->expectException(\RuntimeException::class, 'Username required!')
142+
->assertStatusCode(1)
143+
->assertOutputContains('Could not create user!') // can still make assertions on output before exception was thrown
144+
;
145+
133146
// access result
134147
$result = TestCommand::for(new CreateUserCommand(/** args... */))->execute();
135148

src/TestCommand.php

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Symfony\Component\Console\Application;
66
use Symfony\Component\Console\Command\Command;
7+
use Zenstruck\Assert;
78

89
/**
910
* @author Kevin Bond <kevinbond@gmail.com>
@@ -15,6 +16,10 @@ final class TestCommand
1516

1617
/** @var string[] */
1718
private array $inputs = [];
19+
20+
/** @var callable|class-string|null */
21+
private $expectedException;
22+
private ?string $expectedExceptionMessage = null;
1823
private bool $splitOutputStreams = false;
1924

2025
private function __construct(Command $command, string $cli)
@@ -96,6 +101,25 @@ public function withInput(array $inputs): self
96101
return $this;
97102
}
98103

104+
/**
105+
* Expect executing the command will throw this exception. Fails if not thrown.
106+
*
107+
* @param class-string|callable $expectedException string: class name of the expected exception
108+
* callable: uses the first argument's type-hint
109+
* to determine the expected exception class. When
110+
* exception is caught, callable is invoked with
111+
* the caught exception
112+
* @param string|null $expectedMessage Assert the caught exception message "contains"
113+
* this string
114+
*/
115+
public function expectException($expectedException, ?string $expectedMessage = null): self
116+
{
117+
$this->expectedException = $expectedException;
118+
$this->expectedExceptionMessage = $expectedMessage;
119+
120+
return $this;
121+
}
122+
99123
public function execute(?string $cli = null): CommandResult
100124
{
101125
$autoExit = $this->application->isAutoExitEnabled();
@@ -105,7 +129,7 @@ public function execute(?string $cli = null): CommandResult
105129
$this->application->setAutoExit(false);
106130
$this->application->setCatchExceptions(false);
107131

108-
$status = $this->application->run(
132+
$status = $this->doRun(
109133
$input = new TestInput($cli, $this->inputs),
110134
$output = new TestOutput($this->splitOutputStreams, $input)
111135
);
@@ -115,4 +139,17 @@ public function execute(?string $cli = null): CommandResult
115139

116140
return new CommandResult($cli, $status, $output);
117141
}
142+
143+
private function doRun(TestInput $input, TestOutput $output): int
144+
{
145+
$fn = fn() => $this->application->run($input, $output);
146+
147+
if (!$this->expectedException) {
148+
return $fn();
149+
}
150+
151+
Assert::that($fn)->throws($this->expectedException, $this->expectedExceptionMessage);
152+
153+
return 1;
154+
}
118155
}

tests/FunctionalTest.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
namespace Zenstruck\Console\Test\Tests;
44

5+
use PHPUnit\Framework\AssertionFailedError;
56
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
67
use Symfony\Component\Console\Exception\CommandNotFoundException;
8+
use Zenstruck\Assert;
79
use Zenstruck\Console\Test\InteractsWithConsole;
810
use Zenstruck\Console\Test\Tests\Fixture\FixtureCommand;
911

@@ -136,6 +138,58 @@ public function exceptions_from_commands_are_thrown(): void
136138
$this->consoleCommand('fixture:command --throw')->execute();
137139
}
138140

141+
/**
142+
* @test
143+
*/
144+
public function can_expect_exception(): void
145+
{
146+
$this->consoleCommand('fixture:command --throw')
147+
->expectException(\RuntimeException::class)
148+
->execute()
149+
->assertStatusCode(1)
150+
->assertOutputContains('Executing command...')
151+
->assertOutputContains('Error output.')
152+
;
153+
154+
$this->consoleCommand('fixture:command --throw')
155+
->expectException(\RuntimeException::class, 'Exception thrown!')
156+
->execute()
157+
->assertStatusCode(1)
158+
->assertOutputContains('Executing command...')
159+
->assertOutputContains('Error output.')
160+
;
161+
162+
$this->consoleCommand('fixture:command --throw')
163+
->expectException(function(\RuntimeException $e) {
164+
$this->assertSame('Exception thrown!', $e->getMessage());
165+
})
166+
->execute()
167+
->assertStatusCode(1)
168+
->assertOutputContains('Executing command...')
169+
->assertOutputContains('Error output.')
170+
;
171+
}
172+
173+
/**
174+
* @test
175+
*/
176+
public function if_expected_exception_not_thrown_fail(): void
177+
{
178+
Assert::that(function() {
179+
$this->consoleCommand('fixture:command')
180+
->expectException(\RuntimeException::class)
181+
->execute()
182+
;
183+
})->throws(AssertionFailedError::class, 'No exception thrown. Expected "RuntimeException".');
184+
185+
Assert::that(function() {
186+
$this->consoleCommand('fixture:command --throw')
187+
->expectException(\LogicException::class)
188+
->execute()
189+
;
190+
})->throws(AssertionFailedError::class, 'Expected "LogicException" to be thrown but got "RuntimeException".');
191+
}
192+
139193
/**
140194
* @test
141195
*/

0 commit comments

Comments
 (0)