Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.7] Mock console output while testing to help making assertions #25270

Merged
merged 3 commits into from
Aug 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Illuminate/Console/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ protected function specifyParameters()
public function run(InputInterface $input, OutputInterface $output)
{
return parent::run(
$this->input = $input, $this->output = new OutputStyle($input, $output)
$this->input = $input,
$this->output = $this->laravel->make(OutputStyle::class, ['input' => $input, 'output' => $output])
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,24 @@

namespace Illuminate\Foundation\Testing\Concerns;

use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\PendingCommand;

trait InteractsWithConsole
{
/**
* The list of expected questions with their answers.
*
* @var array
*/
public $expectedQuestions = [];

/**
* The list of expected outputs.
*
* @var array
*/
public $expectedOutput = [];

/**
* Call artisan command and return code.
*
Expand All @@ -15,6 +29,16 @@ trait InteractsWithConsole
*/
public function artisan($command, $parameters = [])
{
return $this->app[Kernel::class]->call($command, $parameters);
$this->beforeApplicationDestroyed(function () {
if (count($this->expectedQuestions)) {
$this->fail('Question "'.array_first($this->expectedQuestions)[0].'" was never asked!');
}

if (count($this->expectedOutput)) {
$this->fail('Output "'.array_first($this->expectedOutput).'" was never printed!');
}
});

return new PendingCommand($this, $this->app, $command, $parameters);
}
}
190 changes: 190 additions & 0 deletions src/Illuminate/Foundation/Testing/PendingCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<?php

namespace Illuminate\Foundation\Testing;

use Mockery;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Console\Kernel;
use PHPUnit\Framework\TestCase as PHPUnit;
use Mockery\Exception\BadMethodCallException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Mockery\Exception\NoMatchingExpectationException;

class PendingCommand
{
/**
* The test being run.
*
* @var \Illuminate\Foundation\Testing\TestCase
*/
public $test;

/**
* The application instance.
*
* @var \Illuminate\Foundation\Application
*/
private $app;

/**
* The command to run.
*
* @var string
*/
private $command;

/**
* The parameters to pass to the command.
*
* @var array
*/
private $parameters;

/**
* The expected exit code.
*
* @var int
*/
private $expectedExitCode;

/**
* Create a new pending console command run.
*
* @param \PHPUnit\Framework\TestCase $test
* @param \Illuminate\Foundation\Application $app
* @param string $command
* @param array $parameters
* @return void
*/
public function __construct(PHPUnit $test, $app, $command, $parameters)
{
$this->test = $test;
$this->app = $app;
$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 an 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 response has the given status code.
*
* @param int $status
* @return $this
*/
public function assertStatus($status)
{
$this->expectedExitCode = $status;

return $this;
}

/**
* Mock the application's console output.
*
* @return void
*/
private function mockTheConsoleOutput()
{
$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()
{
$this->mockTheConsoleOutput();

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!');
}
} catch (BadMethodCallException $e) {
if (str_contains($e->getMessage(), 'askQuestion')) {
$this->test->fail('An an expected question was asked while running the command.');
}
}

if ($this->expectedExitCode != null) {
$this->test->assertTrue(
$exitCode == $this->expectedExitCode,
"Expected status code {$this->expectedExitCode} but received {$exitCode}."
);
}
}
}
9 changes: 8 additions & 1 deletion tests/Database/SeedCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Mockery;
use Illuminate\Database\Seeder;
use PHPUnit\Framework\TestCase;
use Illuminate\Console\OutputStyle;
use Illuminate\Container\Container;
use Illuminate\Database\Console\Seeds\SeedCommand;
use Illuminate\Database\ConnectionResolverInterface;
Expand All @@ -13,6 +14,9 @@ class SeedCommandTest extends TestCase
{
public function testHandle()
{
$input = new \Symfony\Component\Console\Input\ArrayInput(['--force' => true, '--database' => 'sqlite']);
$output = new \Symfony\Component\Console\Output\NullOutput;

$seeder = Mockery::mock(Seeder::class);
$seeder->shouldReceive('setContainer')->once()->andReturnSelf();
$seeder->shouldReceive('setCommand')->once()->andReturnSelf();
Expand All @@ -25,12 +29,15 @@ public function testHandle()
$container->shouldReceive('call');
$container->shouldReceive('environment')->once()->andReturn('testing');
$container->shouldReceive('make')->with('DatabaseSeeder')->andReturn($seeder);
$container->shouldReceive('make')->with('Illuminate\Console\OutputStyle', Mockery::any())->andReturn(
new OutputStyle($input, $output)
);

$command = new SeedCommand($resolver);
$command->setLaravel($container);

// call run to set up IO, then fire manually.
$command->run(new \Symfony\Component\Console\Input\ArrayInput(['--force' => true, '--database' => 'sqlite']), new \Symfony\Component\Console\Output\NullOutput);
$command->run($input, $output);
$command->handle();

$container->shouldHaveReceived('call')->with([$command, 'handle']);
Expand Down
8 changes: 2 additions & 6 deletions tests/Integration/Console/ConsoleApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,14 @@ public function test_artisan_call_using_command_name()
{
$exitCode = $this->artisan('foo:bar', [
'id' => 1,
]);

$this->assertEquals($exitCode, 0);
])->assertStatus(0);
}

public function test_artisan_call_using_command_class()
{
$exitCode = $this->artisan(FooCommandStub::class, [
'id' => 1,
]);

$this->assertEquals($exitCode, 0);
])->assertStatus(0);
}

/*
Expand Down