PHP symfony4: Dependency injection inside KernelTestCase for command - php

Hello I'm trying to create a unit-test for a symfony4 console command but I can't inject the dependencies correctly. I'm very new to symfony4 so maybe this is a basic question for you guys.
My unit test looks like this:
<?php
namespace App\Tests\Command;
use App\Command\ExecuteSalesTaskCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
use Psr\Log\LoggerInterface;
use App\Repository\TaskRepository;
class ExeculteSalesTaskCommandTest extends KernelTestCase
{
/**
* #param LoggerInterface $logger
* #param TaskRepository $taskRepository
*/
public function testExecute(LoggerInterface $logger, TaskRepository $taskRepository)
{
$kernel = self::bootKernel();
$application = new Application($kernel);
$application->add(new ExecuteSalesTaskCommand($logger,$taskRepository));
# UPDATED
$logger = self::$kernel->getContainer()->get(LoggerInterface::class);
$taskRepository = self::$kernel->getContainer()->get(TaskRepository::class);
$command = $application->find('app:execute-sales-task');
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'command' => $command->getName(),
]
);
// the output of the command in the console
$output = $commandTester->getDisplay();
$this->assertContains('Execute sales resulted: ', $output);
}
}
My problem is that I get injection errors like this:
ArgumentCountError: Too few arguments to function
App\Tests\Command\ExeculteSalesTaskCommandTest::testExecute(), 0
passed and exactly 2 expected
UPDATE:
When I fetch the dependencies out of the container I get this kind of error:
There was 1 error:
1) App\Tests\Command\ExeculteSalesTaskCommandTest::testExecute
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException:
The "Psr\Log\LoggerInterface" service or alias has been removed or
inlined when the container was compiled. You should either make it
public, or stop using the container directly and use dependency
injection instead.
How can I inject the necessary dependencies correctly, so I can create an instance of the ExecuteSalesTaskCommand?

Well I found out that the issue was that I tried to load the dependencies manually. Use the autowiring instead like this:
public function testExecute()
{
$dotenv = new Dotenv();
$dotenv->load(__DIR__.'/.env.test');
$kernel = self::bootKernel();
$application = new Application($kernel);
$executeSalesCommand = self::$kernel->getContainer()->get(
'console.command.public_alias.App\Command\ExecuteSalesTaskCommand'
);
$application->add($executeSalesCommand);
$command = $application->find('app:execute-sales-task');
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'command' => $command->getName(),
]
);
// the output of the command in the console
$output = $commandTester->getDisplay();
// do your asserting stuff
}
You need to get the command itself from the kernel container. Now it works.

I was also encountered similar issue. But following worked for me. I don't think you need to add command to the application and again to find it? Please find following solution. It might help others.
<?php
// BaseCommandTester.php
/**
* This is basis for the writing tests, that will cover commands
*
*/
namespace App\Tests\Base;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Dotenv\Dotenv;
class BaseCommandTester extends KernelTestCase
{
/**
* #var
*/
private $application;
/**
* to set test environment and initiate application kernel
*/
public function setUp()
{
/**
* get test env
*/
$dotenv = new Dotenv();
$dotenv->load('/var/www/.env.test');
/**
* boot kernel
*/
$kernel = self::bootKernel();
$this->application = new Application($kernel);
parent::setUp();
}
/**
* #return mixed
*/
public function getApplication()
{
return $this->application;
}
/**
* #param mixed $application
*/
public function setApplication($application)
{
$this->application = $application;
}
}
Test Case
// FeedUpdaterCommandTest.php
<?php
namespace App\Tests\Command;
use App\Tests\Base\BaseCommandTester;
use Symfony\Component\Console\Tester\CommandTester;
class FeedUpdaterCommandTest extends BaseCommandTester
{
/**
* test to update all feeds
*/
public function testExecuteUpdateAll() {
/**
* init command tester and executre
*/
$commandName = 'app:feedUpdater';
$expectedResult = '[OK] Update Success Feed Type : All';
$command = $this->getApplication()->find($commandName);
$commandTester = new CommandTester($command);
$commandTester->execute(array(
'command' => $command->getName()
));
/**
* get result and compare output
*/
$result = trim($commandTester->getDisplay());
$this->assertEquals($result, $expectedResult);
}
}
Result of Test run
#Run tests
root#xxx:/var/www# bin/phpunit tests/Command
#!/usr/bin/env php
PHPUnit 6.5.13 by Sebastian Bergmann and contributors.
Testing tests/Command
2018-11-28T07:47:39+00:00 [alert] Successful update of popularProducts Feeds!
2018-11-28T07:47:39+00:00 [alert] Successful update of topVouchers Feeds!
. 1 / 1 (100%)
Time: 1.44 seconds, Memory: 12.00MB
OK (1 test, 1 assertion)
I am using following Sf4 version
-------------------- --------------------------------------
Symfony
-------------------- --------------------------------------
Version 4.1.7
Service Defination and its by default private
#config/services.yml
App\Service\FeedGenerator:
arguments:
$feeds: '%feed_generator%'
I don't think you need to autowire again.

Related

test command symfony with phpunit

I create some basic command with symfony3.2 to generate some newsletter periodically
I'm dealing with some issue when i want to test my symfony command with phpunit 5.5.4.
It fail from the beginning:
/**
* #param InputInterface $input
* #param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output){
$output->writeln("<info>Script start</info>");
//...
$output->writeln("<info>done</info>");
}
with this unit test:
use MyBundle\Command\MyCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;
class MyCommandTest extends KernelTestCase
{
public function testExecute(){
$kernel = static::createKernel();
$kernel->boot();
$application = new Application($kernel);
$application->add(new MyCommand());
$command = $application->find('generate:newsletter');
$commandTester = new CommandTester($command);
$commandTester->execute(array(
'command' => $command->getName()
));
$output = $commandTester->getDisplay();
$this->assertContains('done',$output);
}
}
I follow this step by step but in my case i get :
Error: Call to a member function writeln() on string
MyBundle/Command/MyCommand.php:197
vendor/symfony/symfony/src/Symfony/Component/Console/Command/Command.php:262
vendor/symfony/symfony/src/Symfony/Component/Console/Tester/CommandTester.php:84
MyBundle/Command/MyCommandTest.php:34
It seems like commandTester don't put correct parameter in execute method from MyCommand.
I'm wondering if it's not CommandTesterClass issue.
That's why i'm here, to share with you that and find some solution together.
Thank you in advance
Method 'getDisplay()' returns a string as you can see from the Api doc:
http://api.symfony.com/3.0/Symfony/Component/Console/Tester/CommandTester.html and you're assigning that string to your $output variable.
I think what you need is 'getOutput()'

Symfony console - displaying help for command with no arguments

I'm developing a pretty simple Symfony console application. It has just one command with one argument, and a few options.
I followed this guide to create an extension of the Application class.
This is the normal usage for the app, and it works fine:
php application <argument>
This also works fine (argument with options):
php application.php <argument> --some-option
If someone runs php application.php without any arguments or options, I want it to run as though the user had run php application.php --help.
I do have a working solution but it isn't optimal and is perhaps slightly brittle. In my extended Application class, I overrode the run() method as follows:
/**
* Override parent method so that --help options is used when app is called with no arguments or options
*
* #param InputInterface|null $input
* #param OutputInterface|null $output
* #return int
* #throws \Exception
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
if ($input === null) {
if (count($_SERVER["argv"]) <= 1) {
$args = array_merge($_SERVER["argv"], ["--help"]);
$input = new ArgvInput($args);
}
}
return parent::run($input, $output);
}
By default, Application::run() is called with a null InputInterface, so here I figured I could just check the raw value of the arguments and forcefully add a help option to pass to the parent method.
Is there a better way to achieve this?
I managed to work out a solution which didn't involve touching the Application class at all. To call the help command from within another command:
/**
* #param InputInterface $input
* #param OutputInterface $output
* #return int
* #throws \Symfony\Component\Console\Exception\ExceptionInterface
*/
protected function outputHelp(InputInterface $input, OutputInterface $output)
{
$help = new HelpCommand();
$help->setCommand($this);
return $help->run($input, $output);
}
To do a specific action depending on command, you can use an EventListener which is called when the onConsoleCommand is fired.
The listener class should work as follows :
<?php
namespace AppBundle\EventListener;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Command\HelpCommand;
class ConsoleEventListener
{
public function onConsoleCommand(ConsoleCommandEvent $event)
{
$application = $event->getCommand()->getApplication();
$inputDefinition = $application->getDefinition();
if ($inputDefinition->getArgumentCount() < 2) {
$help = new HelpCommand();
$help->setCommand($event->getCommand());
return $help->run($event->getInput(), $event->getOutput());
}
}
}
The service declaration :
services:
# ...
app.console_event_listener:
class: AppBundle\EventListener\ConsoleEventListener
tags:
- { name: kernel.event_listener, event: console.command, method: onConsoleCommand }

Unit test UploadedFile.php in PHPSpec

I am trying to test a data transfer object class which constructs with Symfony\Component\HttpFoundation\File\UploadedFile.php class in Laravel. I've had no luck testing this implementation in PHPSpec. Here is what I have so far:
Test
public function let($event_id)
{
$prophet = new Prophet;
$prophecy = $prophet->prophesize();
$prophecy->willBeConstructedWith(array('path', 'name'));
$prophecy->willExtend('Symfony\Component\HttpFoundation\File\UploadedFile');
$attendee_list = $prophecy->reveal();
$this->beConstructedWith($event_id, $attendee_list);
}
public function it_is_initializable()
{
$this->shouldHaveType('Captix\Attendees\ImportAttendeesCommand');
}
Class
class ImportAttendeesCommand
{
/**
* Event primary key
* #var $event_id
*/
protected $event_id;
/**
* CSV file
* #var $attendee_list
*/
protected $attendee_list;
/**
* Constructor for ImportAttendeesCommand
*/
public function __construct($event_id, UploadedFile $attendee_list)
{
$this->event_id = $event_id;
$this->attendee_list = $attendee_list;
}
}
Result
2 -_-__,------,
0 -_-__| /\_/\
0 -_-_^|__( o .o)
1 -_-_ "" ""
Captix/Attendees/ImportAttendeesCommand
38 ! it is initializable
exception [exc:Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException("The file "path" does not exist")] has been thrown.
0 vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/File/File.php:41
throw new Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException("The file "path" does not ...")
1 vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/File/UploadedFile.php:100
Symfony\Component\HttpFoundation\File\File->__construct("path", true)
2 vendor/phpspec/prophecy/src/Prophecy/Doubler/Generator/ClassCreator.php(49) : eval()'d code:6
Symfony\Component\HttpFoundation\File\UploadedFile->__construct("path", "name", null, null, null, null)
3 [internal]
Double\Symfony\Component\HttpFoundation\File\UploadedFile\P4->__construct("path", "name")
4 vendor/phpspec/prophecy/src/Prophecy/Doubler/Doubler.php:109
ReflectionClass->newInstanceArgs([array:2])
5 vendor/phpspec/prophecy/src/Prophecy/Doubler/LazyDouble.php:119
Prophecy\Doubler\Doubler->double([obj:ReflectionClass], [array:0], [array:2])
6 vendor/phpspec/prophecy/src/Prophecy/Prophecy/ObjectProphecy.php:114
Prophecy\Doubler\LazyDouble->getInstance()
7 app/Spec/Captix/Attendees/ImportAttendeesCommandSpec.php:33
Prophecy\Prophecy\ObjectProphecy->reveal()
8 [internal]
Spec\Captix\Attendees\ImportAttendeesCommandSpec->let([obj:PhpSpec\Wrapper\Collaborator])
3 examples (2 passed, 1 broken)
119ms
I've tried several ways to test this, using Mockery, Prohpecy, stdClass, nothing seems to be working. Any help on this would be much appreciated. I am welcome to any implementation of this test. Thanks to all in advance.
Create an interface:
interface UploadedFileInterface
{}
Create class that implements this interface and extends Symfony class.
class MyUploadedFile extends UploadedFile implements UploadedFileInterface
{}
Change constructor in your command:
public function __construct($event_id, UploadedFileInterface $attendee_list)
{
$this->event_id = $event_id;
$this->attendee_list = $attendee_list;
}
Now You can easily test your class in isolation:
public function let($event_id, UploadedFileInterface $attendeeList)
{
$this->beConstructedWith($event_id, $attendeeList);
}
And if some day you will have to switch from Symfonys UploadedFile to another implementation, the only place that you will have to change will be MyUploadedFile class.
I had the same issue using prophecy to mock the UploadedFile so I submitted an issue for feedback (https://github.com/phpspec/prophecy/issues/184).
The solution is 'do not mock what you do not own'. I'm not sure yet how you would put that into practice, but thought you might be interested.

Troubles with GO! Aspect-Oriented Framework

If someone work with GO! framework, can you help me.
I install framework on php 5.3.13. Demo example is working.
But my own example doesn't work. Aspect(method beforeMethodExecution) is not perfomed.
Here is my code.
Main file:
//1 Include kernel and all classes
if (file_exists(__DIR__ .'/../../vendor/autoload.php')) {
$loader = include __DIR__ .'/../../vendor/autoload.php';
}
// 2 Make own ascpect kernel
use Go\Core\AspectKernel;
use Go\Core\AspectContainer;
class Kernel extends AspectKernel{
/**
* Configure an AspectContainer with advisors, aspects and pointcuts
*
* #param AspectContainer $container
*
* #return void
*/
public function configureAop(AspectContainer $container)
{
}
}
//3 Initiate aspect kernel
$Kernel = Kernel::getInstance();
$Kernel->init();
//4 Include aspect
include(__DIR__.'/aspectclass/AspectClass.php');
$aspect = new DebugAspect();
//5 register aspect
$Kernel->getContainer()->registerAspect($aspect);
//6 Include test class
include(__DIR__.'/class/class1.php');
//7 Execute test class
$Class = new General('test');
$Class->publicHello();
File with test class:
class General{
protected $message = '';
public function __construct($message)
{
$this->message = $message;
}
public function publicHello()
{
echo 'Hello, you have a public message: ', $this->message, "<br>", PHP_EOL;
}
}
File with aspect:
use Go\Aop\Aspect;
use Go\Aop\Intercept\FieldAccess;
use Go\Aop\Intercept\FunctionInvocation;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;
use Go\Lang\Annotation\DeclareParents;
use Go\Lang\Annotation\DeclareError;
class DebugAspect implements Aspect{
/**
* Method that should be called before real method
*
* #param MethodInvocation $invocation Invocation
* #Before("execution(General->*(*))")
*
*/
public function beforeMethodExecution(MethodInvocation $invocation)
{
$obj = $invocation->getThis();
echo 'Calling Before Interceptor for method: ',
is_object($obj) ? get_class($obj) : $obj,
$invocation->getMethod()->isStatic() ? '::' : '->',
$invocation->getMethod()->getName(),
'()',
' with arguments: ',
json_encode($invocation->getArguments()),
PHP_EOL;
}
}
As you know, go-aop isn't a PHP extension, so it couldn't transform classes that were loaded directly via require or include. Internally it tries to overwrite the source code on-the-fly, but it should receive a control (via integration with composer or custom autoloader class).
So, you have an error here:
//6 Include test class
include(__DIR__.'/class/class1.php');
You explicitly load this class into memory and there is no way to transform it from userland. To pass a control to the framework, you should make this explicitly. Look at the line AopComposerLoader.php#L99 to have an idea how it works. Here we include a source file via the stream source filter that pass control to the framework and it can transform the class to weave an aspects.
To fix your example just change an include to the following:
include (FilterInjectorTransformer::rewrite(__DIR__.'/class/class1.php'));

Doctrine annotation loader fails

I'm trying to run JMSSerializer. My simple code
use JMS\Serializer\Annotation\Type;
class Comment
{
private $msg;
public function __construct($msg)
{
$this->msg = $msg;
}
}
class Person
{
/**
* #Type("array<Comment>")
*/
private $commentList;
public function addComment(Comment $comment)
{
$this->commentList[] = $comment;
}
}
$type = new Type;
$serializer = JMS\Serializer\SerializerBuilder::create()->build();
$data = new Person();
$data->addComment(new Comment('hey'));
var_dump($serializer->serialize($data, 'json'));
fails with
PHP Fatal error: Uncaught exception 'Doctrine\Common\Annotations\AnnotationException' with message '[Semantical Error] The annotation "#JMS\Serializer\Annotation\Type" in property Person::$commentList does not exist, or could not be auto-loaded.' in xxx.php:52
OK, but if I add line
$type = new Type;
to trigger autoloader manually, it works:
string(32) "{"comment_list":[{"msg":"hey"}]}"
As I see AnnotationRegistry doesn't use autoloader, it tries to use some own autoloader. It looks ugly, what do I have to do to fix it?
OK, I answer my question myself. I have to register annotations somewhere in autoloader file:
\Doctrine\Common\Annotations\AnnotationRegistry::registerAutoloadNamespace(
'JMS\Serializer\Annotation', __DIR__.'/vendor/jms/serializer/src'
);
Other ways: http://docs.doctrine-project.org/projects/doctrine-common/en/latest/reference/annotations.html#registering-annotations
A full configuration sample for the standalone JMS serializer library could be:
<?php
namespace iMSCP\Service;
use JMS\Serializer\Serializer;
use JMS\Serializer\SerializerBuilder;
use Doctrine\Common\Annotations\AnnotationRegistry;
use iMSCP_Registry as Registry;
/**
* Class SerializerServiceFactory
* #package iMSCP\Service
*/
class SerializerServiceFactory
{
/**
* #var Serializer
*/
static $serialiszer;
public static function create()
{
if (static::$serialiszer === null) {
$config = Registry::get('config');
AnnotationRegistry::registerAutoloadNamespace(
'JMS\Serializer\Annotation', $config['CACHE_DATA_DIR'] . '/packages/vendor/jms/serializer/src'
);
static::$serialiszer = SerializerBuilder::create()
->setCacheDir(CACHE_PATH . '/serializer')
->setDebug($config['DEVMODE'])
->build();
}
return static::$serialiszer;
}
}
Here, I register the JMS\Serializer\Annotation namespace using the Annotation registry as provided by Doctrine. Once done, all is working as expected.

Categories