Laravel 5 console (artisan) command unit tests - php

I am migrating my Laravel 4.2 app to 5.1 (starting with 5.0) and am a lot of trouble with my console command unit tests. I have artisan commands for which I need to test the produced console output, proper question/response handling and interactions with other services (using mocks). For all its merits, the Laravel doc is unfortunately silent with regards to testing console commands.
I finally found a way to create those tests, but it feels like a hack with those setLaravel and setApplication calls.
Is there a better way to do this? I wish I could add my mock instances to the Laravel IoC container and let it create the commands to test with everything properly set. I'm afraid my unit tests will break easily with newer Laravel versions.
Here's my unit test:
Use statements:
use Mockery as m;
use App\Console\Commands\AddClientCommand;
use Symfony\Component\Console\Tester\CommandTester;
Setup
public function setUp() {
parent::setUp();
$this->store = m::mock('App\Services\Store');
$this->command = new AddClientCommand($this->store);
// Taken from laravel/framework artisan command unit tests
// (e.g. tests/Database/DatabaseMigrationRollbackCommandTest.php)
$this->command->setLaravel($this->app->make('Illuminate\Contracts\Foundation\Application'));
// Required to provide input to command questions (provides command->getHelper())
// Taken from ??? when I first built my command tests in Laravel 4.2
$this->command->setApplication($this->app->make('Symfony\Component\Console\Application'));
}
Input provided as command arguments. Checks console output
public function testReadCommandOutput() {
$commandTester = new CommandTester($this->command);
$result = $commandTester->execute([
'--client-name' => 'New Client',
]);
$this->assertSame(0, $result);
$templatePath = $this->testTemplate;
// Check console output
$this->assertEquals(1, preg_match('/^Client \'New Client\' was added./m', $commandTester->getDisplay()));
}
Input provided by simulated keyboard keys
public function testAnswerQuestions() {
$commandTester = new CommandTester($this->command);
// Simulate keyboard input in console for new client
$inputs = $this->command->getHelper('question');
$inputs->setInputStream($this->getInputStream("New Client\n"));
$result = $commandTester->execute([]);
$this->assertSame(0, $result);
$templatePath = $this->testTemplate;
// Check console output
$this->assertEquals(1, preg_match('/^Client \'New Client\' was added./m', $commandTester->getDisplay()));
}
protected function getInputStream($input) {
$stream = fopen('php://memory', 'r+', false);
fputs($stream, $input);
rewind($stream);
return $stream;
}
updates
This doesn't work in Laravel 5.1 #11946

I have done this before as follows - my console command returns a json response:
public function getConsoleResponse()
{
$kernel = $this->app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArrayInput([
'command' => 'test:command', // put your command name here
]),
$output = new Symfony\Component\Console\Output\BufferedOutput
);
return json_decode($output->fetch(), true);
}
So if you want to put this in it's own command tester class, or as a function within TestCase etc... up to you.

use Illuminate\Support\Facades\Artisan;
use Symfony\Component\Console\Output\BufferedOutput;
$output = new BufferedOutput();
Artisan::call('passport:client', [
'--password' => true,
'--name' => 'Temp Client',
'--no-interaction' => true,
], $output);
$stringOutput = $output->fetch();

Related

Code coverage from a php built in server is not correct

Objective
I am writing phpunits tests, and I want to get the code coverage.
Problem
To do End To End tests, I have a Php build in server to setup my API, then the tests call this api.
But everything that is being executed by the server is not in the report.
For exemple I tested a controller with api calls, and on my report there is 0%.
All the file that are tested without the Php build int server are ok, it's only the tests that works with that are not counted.
Is there a way to count it ?
Some code
I am using a php that create an http server to do end to end tests on my api.
Here is my class
<?php
use GuzzleHttp\Client;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Process\Process;
class ApiTestCase extends \PHPUnit\Framework\TestCase
{
protected static string $public_directory;
protected static Process $process;
const ENVIRONMENT = 'test';
const HOST = '0.0.0.0';
const PORT = 9876; // Adjust this to a port you're sure is free
public static function setUpBeforeClass(): void
{
$command = [
'php',
'-d',
'variables_order=EGPCS',
'-S',
self::HOST . ':' . self::PORT,
'-t',
self::$public_directory,
self::$public_directory.'/index.php'
];
// Using Symfony/Process to get a handler for starting a new process
self::$process = new Process($command, null, [
'APP_ENV' => self::ENVIRONMENT
]);
// Disabling the output, otherwise the process might hang after too much output
self::$process->disableOutput();
// Actually execute the command and start the process
self::$process->start();
// Let's give the server some leeway to fully start
usleep(100000);
}
public static function tearDownAfterClass(): void
{
self::$process->stop();
}
/**
* #param array<string,mixed>|null $data
* #param string $path
* #param string $method
* #return ResponseInterface
* #throws \GuzzleHttp\Exception\GuzzleException
*/
protected function dispatch(string $path, string $method = 'POST', ?array $data = null): ResponseInterface
{
$params = [];
if ($data) {
$params['form_params'] = $data;
}
$client = new Client(['base_uri' => 'http://127.0.0.1:' . self::PORT]);
return $client->request($method, $path, $params);
}
}
So in my test i can use it like that and it works fine
class MyApiTest extends \App\Tests\ApiDbTestCase
{
public function testAuthClientSuccess()
{
// creation of $parameters
$res = $this->dispatch('/v1/user/my/url', 'POST', $parameters);
// My asserts are done after that
}
}
I am using github action to create
name: Phpunit coverage
on: [push]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout#v2
- uses: php-actions/composer#v6
- uses: php-actions/phpunit#v3
with:
php_extensions: pdo_pgsql xdebug
args: --coverage-html
env:
XDEBUG_MODE: coverage
- name: Archive code coverage results
uses: actions/upload-artifact#v2
with:
name: code-coverage-report
path: output/code-coverage

Symfony - Best practice to reset the database

I'm working on a Symfony 4.2 project and I'm looking for the best practice to achieve a reset of the database when the admin needs to do it via a button in backoffice.
Explanation :
The project is a temporary event website.
This means that, people will visit the website only for a day / a week and then the website is off. Per example, a website for spectators into a stadium during a basketball tournament.
When the tournament is over, the administrator would like to reset all datas sent during it via a button.
Right now I did it like this but I don't know if it's the better way in production environment.
I created a service which get the KernelInterface in constructor :
public function resetDB() {
$application = new Application($this->kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'doctrine:schema:drop',
'--force' => true
]);
$output = new BufferedOutput();
$application->run($input, $output);
$responseDrop = $output->fetch();
if (strpos($responseDrop, 'successfully') !== false) {
$input = new ArrayInput([
'command' => 'doctrine:schema:create',
]);
$application->run($input, $output);
$responseCreate = $output->fetch();
if (strpos($responseCreate, 'successfully') !== false)
return new Response();
}
return new \ErrorException();
}
Firstly, is it good to do it like this in a production environment ? (Nobody else the administrator will use the website when doing this operation)
Secondly, I'm not really satisfied with the method I used to check if the operation has been successfully done (strpos($responseCreate, 'successfully') !== false). Does someone know a better way ?
Thanks a lot for your help
If it works for you, its ok. About the "successful" check part. Simply surround your call in a try-catch block and check for exceptions. If no exception was thrown, assume it did execute successfully.
$application = new Application($this->kernel);
$application->setAutoExit(false);
try {
$application->run(
new StringInput('doctrine:schema:drop --force'),
new DummyOutput()
);
$application->run(
new StringInput('doctrine:schema:create'),
new DummyOutput()
);
return new Response();
} catch (\Exception $exception) {
// don't throw exceptions, use proper responses
// or do whatever you want
return new Response('', Response::HTTP_INTERNAL_SERVER_ERROR);
}
Is PostgreSQL good enough at DDL transactions? Force a transaction then:
$application = new Application($this->kernel);
$application->setAutoExit(false);
// in case of any SQL error
// an exception will be thrown
$this->entityManager->transactional(function () use ($application) {
$application->run(
new StringInput('doctrine:schema:drop --force'),
new DummyOutput()
);
$application->run(
new StringInput('doctrine:schema:create'),
new DummyOutput()
);
});
return new Response();
I'm not sure about the way you executing the commands, but there is a single command alternative to consider, using DoctrineFixturesBundle. You need to install it for use in the production environment (technically not recommended, I think because of risk of deleting prod data, but that's what you want to do).
Install:
$ composer require doctrine/doctrine-fixtures-bundle
Config:
// config/bundles.php
return [
...
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['all' => true],
...
];
You do need to create a fixture and it must have a load() method that is compatible with Doctrine\Common\DataFixtures\FixtureInterface::load(Doctrine\Common\Persistence\ObjectManager $manager), but it can be empty exactly like below:
<?php // src/DataFixtures/AppFixtures.php
namespace App\DataFixtures;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\Persistence\ObjectManager;
class AppFixtures extends Fixture
{
public function load(ObjectManager $manager){}
}
The command:
$ php bin/console doctrine:fixtures:load -n --purge-with-truncate --env=prod
Help:
$ php bin/console doctrine:fixtures:load --help
Firstly, is it good to do it like this in a production environment ?
I don't think so! For example the commands below would warn you with: [CAUTION] This operation should not be executed in a production environment!. However, everything is possible in wild programming world as shown below.
Try Symfony's The Process Component.
This is the basic example so it is up to you make it cleaner and duplication free. I tested and it works. You can stream the output as well.
# DROP IT
$process = new Process(
['/absolute/path/to/project/bin/console', 'doctrine:schema:drop', '--force', '--no-interaction']
);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
# RECREATE IT
$process = new Process(
['/absolute/path/to/project/bin/console', 'doctrine:schema:update', '--force', '--no-interaction']
);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}

Symfony Process Component - Set Input Argument for command

I need to run a command asynchronously. To do this, I'm trying to use Process Component.
The command I'm trying to start is calling a function that needs somes parameters. These parameters are given by Controller that launches the Process.
Problem is I don't know how to pass parameters to my command with Process Component.
Command :
protected function configure()
{
$this
->setName('generationpdf:classement')
->setDescription('Génère le PDF d\'un classement.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
ini_set('memory_limit','4000M');
$pdf = $this->imprimerAction();
$this->sendClassement($pdf);
$output->writeln('PDF envoyé.');
}
Controller :
$command = new PDFClassementCommand();
$input = new ArrayInput(array('id' => $id, 'errata' => $errata, 'precision' => $precision, 'group' => $group, 'logoAnnee' => $logoAnnee));
$output = new NullOutput();
$process = new Process($command);
$process->disableOutput();
$process->start();
Parameters I need to use are in ArrayInput but Process doesn't take array argument.
The process is expecting first argument to be string.
Example:
$process = new Process('ls');
To run command you will need to execute symfony console command
$process = new Process('/var/www/my-project/bin/console generationpdf:classement')
You don't have to hardcode the path, see how-to-get-the-root-dir.
You can pass parameters normally, like running command from cli
$process = new Process('/var/www/my-project/bin/console generationpdf:classement --id=5 --errata=someValue')

How to run console command in yii2 from web

I have created a console command in console/controllers with SuggestionController .
If i run command like php yii suggestions, its working.
I want to know how to execute console command from web without any extensions of yii2.
It can be done much simpler
$oldApp = \Yii::$app;
new \yii\console\Application([
'id' => 'Command runner',
'basePath' => '#app',
'components' => [
'db' => $oldApp->db,
],
);
\Yii::$app->runAction('migrate/up', ['migrationPath' => '#yii/rbac/migrations/', 'interactive' => false]);
\Yii:$app = $oldApp;
Github LINK
As of Yii2 - 2.0.11.2 advanced app -- this works
First let's make sure controller and namespace correct. In this case frontend app accessing console application import method()
In console\controllers\FhirController
Set the alias to be available in the console\config\main.php [OPTIONAL]
'aliases' => [
'#common' => dirname(__DIR__),
'#frontend' => dirname(dirname(__DIR__)) . '/frontend',
'#backend' => dirname(dirname(__DIR__)) . '/backend',
'#console' => dirname(dirname(__DIR__)) . '/console',
],
Finally from the frontend view, make the call like this:
In this case, calling the controller route fhir then method import()
$consoleController = new console\controllers\FhirController('fhir', Yii::$app);
$consoleController->runAction('import');
On a site I'm overhauling, I have a need for a background task, that can be toggled via an action, which requires that I can also find its pid using ps. After much googling and almost as much swearing, I pieced together the solution.
# First change to (already-calculated) correct dir:
chdir($strPath);
# Now execute with nohup, directed to dev/null, and crucially with & at end, to run async:
$output = shell_exec("nohup php yii <console controller>/<action> > /dev/null &");
Yes, I understand that shell_exec should be used with extreme caution, thanks.
This is way I found and used some time ago to run yii console controller/action (I used this for run migrations from web).
In your web controller action:
// default console commands outputs to STDOUT
// so this needs to be declared for wep app
if (! defined('STDOUT')) {
define('STDOUT', fopen('/tmp/stdout', 'w'));
}
$consoleController = new \yii\console\controllers\SuggestionController;
$consoleController->runAction('your action eg. index');
/**
* open the STDOUT output file for reading
*
* #var $message collects the resulting messages of the migrate command to be displayed in a view
*/
$handle = fopen('/tmp/stdout', 'r');
$message = '';
while (($buffer = fgets($handle, 4096)) !== false) {
$message .= $buffer . "\n";
}
fclose($handle);
return $message;
You can either exec() your command ´´´php yii suggestions´´´ but this may result in permission issues with the webserver user.
The better way is to use a ConsoleRunner extension, e.g. yii2-console-runner or yii2-console-runner-extension which do the job control job a little bit more sophisticated and more secure with popen().
Always be aware of code injections when executing exec() and the like!
I think this is the simplest solution:
$controller = new SuggestionController(Yii::$app->controller->id, Yii::$app);
$controller->actionSuggestions();
yii2 call console command from web or api .
my controller name :
UnitController
in folder
console/controllers
my action name :
actionUnitDesc
if(!defined('STDIN')) define('STDIN', fopen('php://stdin', 'rb'));
if(!defined('STDOUT')) define('STDOUT', fopen('php://stdout', 'wb'));
if(!defined('STDERR')) define('STDERR', fopen('php://stderr', 'wb'));
$consoleController = new \console\controllers\UnitController('unit', Yii::$app);
$consoleController->runAction('unit-des');

In Symfony2 , How do you mock a Console Component which is consuming another ConsoleComponent

I'm writing a console component using Symfony2 libraries that consumes another application that's also written with symfony2 console components.
I want to mock the other applications console component, how do I go about achieving this? The application I'm building is simply consuming an existing command from another application:
Basically, how do you write a unit test for the code below:
protected function execute(InputInterface $input, OutputInterface $output)
{
$command = $this->getApplication()->find('demo:greet');
$arguments = array(
'command' => 'demo:greet',
'name' => 'Fabien',
'--yell' => true,
);
$input = new ArrayInput($arguments);
$returnCode = $command->run($input, $output);
// ...
}
Another application or the same application?
$commandMock = $this->getMock('Symfony\Component\Console\Command\Command');
$commandMock->expects($this->once())->method('run')->with(...)->will($this->returnValue(1));
$applicationMock = $this->getMockBuilder('Symfony\Component\Console\Application')
->disableConstructor()->getMock();
$applicationMock->method('find')
->with($this->equalTo('demo:greet'))
->will($this->returnValue($commandMock));
You just mock the command and mock the application to return the mocked command.

Categories