PendingCommand.php
TLDR
The PendingCommand
class in the Illuminate\Testing
namespace is used for creating pending console command runs in Laravel tests. It allows specifying expected input, output, table displays, and exit codes for the command being tested.
Methods
expectsQuestion
Specifies an expected question that will be asked when the command runs.
expectsConfirmation
Specifies an expected confirmation question that will be asked when the command runs.
expectsChoice
Specifies an expected choice question with expected answers that will be asked/shown when the command runs.
expectsOutput
Specifies output that should be printed when the command runs.
doesntExpectOutput
Specifies output that should never be printed when the command runs.
expectsOutputToContain
Specifies that the given string should be contained in the command output.
doesntExpectOutputToContain
Specifies that the given string shouldn't be contained in the command output.
expectsTable
Specifies a table that should be printed when the command runs.
assertExitCode
Asserts that the command has the given exit code.
assertNotExitCode
Asserts that the command does not have the given exit code.
assertSuccessful
Asserts that the command has the success exit code.
assertOk
Alias for assertSuccessful
.
assertFailed
Asserts that the command does not have the success exit code.
execute
Executes the command and returns the exit code.
run
Executes the command and returns the exit code. This method is also called by __destruct
if the command has not been executed yet.
Classes
There are no additional classes in this file.
<?php
namespace Illuminate\Testing;
use Illuminate\Console\OutputStyle;
use Illuminate\Console\PromptValidationException;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
use Mockery;
use Mockery\Exception\NoMatchingExpectationException;
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
class PendingCommand
{
/**
* The test being run.
*
* @var \Illuminate\Foundation\Testing\TestCase
*/
public $test;
/**
* The application instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $app;
/**
* The command to run.
*
* @var string
*/
protected $command;
/**
* The parameters to pass to the command.
*
* @var array
*/
protected $parameters;
/**
* The expected exit code.
*
* @var int
*/
protected $expectedExitCode;
/**
* The unexpected exit code.
*
* @var int
*/
protected $unexpectedExitCode;
/**
* Determine if the command has executed.
*
* @var bool
*/
protected $hasExecuted = false;
/**
* Create a new pending console command run.
*
* @param \PHPUnit\Framework\TestCase $test
* @param \Illuminate\Contracts\Container\Container $app
* @param string $command
* @param array $parameters
* @return void
*/
public function __construct(PHPUnitTestCase $test, Container $app, $command, $parameters)
{
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
/**
* Specify an expected question that will be asked when the command runs.
*
* @param string $question
* @param string|bool $answer
* @return $this
*/
public function expectsQuestion($question, $answer)
{
$this->test->expectedQuestions[] = [$question, $answer];
return $this;
}
/**
* Specify an expected confirmation question that will be asked when the command runs.
*
* @param string $question
* @param string $answer
* @return $this
*/
public function expectsConfirmation($question, $answer = 'no')
{
return $this->expectsQuestion($question, strtolower($answer) === 'yes');
}
/**
* Specify an expected choice question with expected answers that will be asked/shown when the command runs.
*
* @param string $question
* @param string|array $answer
* @param array $answers
* @param bool $strict
* @return $this
*/
public function expectsChoice($question, $answer, $answers, $strict = false)
{
$this->test->expectedChoices[$question] = [
'expected' => $answers,
'strict' => $strict,
];
return $this->expectsQuestion($question, $answer);
}
/**
* 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;
}
/**
* Specify output that should never be printed when the command runs.
*
* @param string $output
* @return $this
*/
public function doesntExpectOutput($output)
{
$this->test->unexpectedOutput[$output] = false;
return $this;
}
/**
* Specify that the given string should be contained in the command output.
*
* @param string $string
* @return $this
*/
public function expectsOutputToContain($string)
{
$this->test->expectedOutputSubstrings[] = $string;
return $this;
}
/**
* Specify that the given string shouldn't be contained in the command output.
*
* @param string $string
* @return $this
*/
public function doesntExpectOutputToContain($string)
{
$this->test->unexpectedOutputSubstrings[$string] = false;
return $this;
}
/**
* Specify a table that should be printed when the command runs.
*
* @param array $headers
* @param \Illuminate\Contracts\Support\Arrayable|array $rows
* @param string $tableStyle
* @param array $columnStyles
* @return $this
*/
public function expectsTable($headers, $rows, $tableStyle = 'default', array $columnStyles = [])
{
$table = (new Table($output = new BufferedOutput))
->setHeaders((array) $headers)
->setRows($rows instanceof Arrayable ? $rows->toArray() : $rows)
->setStyle($tableStyle);
foreach ($columnStyles as $columnIndex => $columnStyle) {
$table->setColumnStyle($columnIndex, $columnStyle);
}
$table->render();
$lines = array_filter(
explode(PHP_EOL, $output->fetch())
);
foreach ($lines as $line) {
$this->expectsOutput($line);
}
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;
}
/**
* Assert that the command does not have the given exit code.
*
* @param int $exitCode
* @return $this
*/
public function assertNotExitCode($exitCode)
{
$this->unexpectedExitCode = $exitCode;
return $this;
}
/**
* Assert that the command has the success exit code.
*
* @return $this
*/
public function assertSuccessful()
{
return $this->assertExitCode(Command::SUCCESS);
}
/**
* Assert that the command has the success exit code.
*
* @return $this
*/
public function assertOk()
{
return $this->assertSuccessful();
}
/**
* Assert that the command does not have the success exit code.
*
* @return $this
*/
public function assertFailed()
{
return $this->assertNotExitCode(Command::SUCCESS);
}
/**
* Execute the command.
*
* @return int
*/
public function execute()
{
return $this->run();
}
/**
* Execute the command.
*
* @return int
*
* @throws \Mockery\Exception\NoMatchingExpectationException
*/
public function run()
{
$this->hasExecuted = true;
$mock = $this->mockConsoleOutput();
try {
$exitCode = $this->app->make(Kernel::class)->call($this->command, $this->parameters, $mock);
} catch (NoMatchingExpectationException $e) {
if ($e->getMethodName() === 'askQuestion') {
$this->test->fail('Unexpected question "'.$e->getActualArguments()[0]->getQuestion().'" was asked.');
}
throw $e;
} catch (PromptValidationException) {
$exitCode = Command::FAILURE;
}
if ($this->expectedExitCode !== null) {
$this->test->assertEquals(
$this->expectedExitCode, $exitCode,
"Expected status code {$this->expectedExitCode} but received {$exitCode}."
);
} elseif (! is_null($this->unexpectedExitCode)) {
$this->test->assertNotEquals(
$this->unexpectedExitCode, $exitCode,
"Unexpected status code {$this->unexpectedExitCode} was received."
);
}
$this->verifyExpectations();
$this->flushExpectations();
return $exitCode;
}
/**
* Determine if expected questions / choices / outputs are fulfilled.
*
* @return void
*/
protected function verifyExpectations()
{
if (count($this->test->expectedQuestions)) {
$this->test->fail('Question "'.Arr::first($this->test->expectedQuestions)[0].'" was not asked.');
}
if (count($this->test->expectedChoices) > 0) {
foreach ($this->test->expectedChoices as $question => $answers) {
$assertion = $answers['strict'] ? 'assertEquals' : 'assertEqualsCanonicalizing';
$this->test->{$assertion}(
$answers['expected'],
$answers['actual'],
'Question "'.$question.'" has different options.'
);
}
}
if (count($this->test->expectedOutput)) {
$this->test->fail('Output "'.Arr::first($this->test->expectedOutput).'" was not printed.');
}
if (count($this->test->expectedOutputSubstrings)) {
$this->test->fail('Output does not contain "'.Arr::first($this->test->expectedOutputSubstrings).'".');
}
if ($output = array_search(true, $this->test->unexpectedOutput)) {
$this->test->fail('Output "'.$output.'" was printed.');
}
if ($output = array_search(true, $this->test->unexpectedOutputSubstrings)) {
$this->test->fail('Output "'.$output.'" was printed.');
}
}
/**
* Mock the application's console output.
*
* @return \Mockery\MockInterface
*/
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) {
if (isset($this->test->expectedChoices[$question[0]])) {
$this->test->expectedChoices[$question[0]]['actual'] = $argument->getAutocompleterValues();
}
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;
});
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]);
});
}
foreach ($this->test->expectedOutputSubstrings as $i => $text) {
$mock->shouldReceive('doWrite')
->atLeast()
->times(0)
->withArgs(fn ($output) => str_contains($output, $text))
->andReturnUsing(function () use ($i) {
unset($this->test->expectedOutputSubstrings[$i]);
});
}
foreach ($this->test->unexpectedOutput as $output => $displayed) {
$mock->shouldReceive('doWrite')
->atLeast()
->times(0)
->ordered()
->with($output, Mockery::any())
->andReturnUsing(function () use ($output) {
$this->test->unexpectedOutput[$output] = true;
});
}
foreach ($this->test->unexpectedOutputSubstrings as $text => $displayed) {
$mock->shouldReceive('doWrite')
->atLeast()
->times(0)
->withArgs(fn ($output) => str_contains($output, $text))
->andReturnUsing(function () use ($text) {
$this->test->unexpectedOutputSubstrings[$text] = true;
});
}
return $mock;
}
/**
* Flush the expectations from the test case.
*
* @return void
*/
protected function flushExpectations()
{
$this->test->expectedOutput = [];
$this->test->expectedOutputSubstrings = [];
$this->test->unexpectedOutput = [];
$this->test->unexpectedOutputSubstrings = [];
$this->test->expectedTables = [];
$this->test->expectedQuestions = [];
$this->test->expectedChoices = [];
}
/**
* Handle the object's destruction.
*
* @return void
*/
public function __destruct()
{
if ($this->hasExecuted) {
return;
}
$this->run();
}
}