How to test dependency injection with PHPUnit on symfony 4 - symfony

In a symfony 4 project, many services/controllers need log.
Trying to use the advantage of traits & autowire options given by symfony, I created a loggerTrait that will be passed to the different services.
namespace App\Helper;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
trait LoggerTrait
{
/** #var LoggerInterface */
private $logger;
/** #var array */
private $context = [];
/**
* #return LoggerInterface
*/
public function getLogger(): LoggerInterface
{
return $this->logger;
}
/**
* #required
*
* #param LoggerInterface|null $logger
*/
public function setLogger(?LoggerInterface $logger): void
{
$this->logger = $logger;
}
public function logDebug(string $message, array $context = []): void
{
$this->log(LogLevel::DEBUG, $message, $context);
}
...
}
(inspired by symfonycasts.com)
A service will be using this trait
namespace App\Service;
use App\Helper\LoggerTrait;
class BaseService
{
use LoggerTrait;
/** #var string */
private $name;
public function __construct(string $serviceName)
{
$this->name = $serviceName;
}
public function logName()
{
$this->logInfo('Name of the service', ['name' => $this->name]);
}
}
It works perfectly but I couldn't succeed to test it.
I tried to extend KernelTestCase in my test to mock an loggerInterface but I receive Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "Psr\Log\LoggerInterface" service is private, you cannot replace it which make perfect sens.
Here my test:
namespace App\Tests\Service;
use App\Service\BaseService;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class BaseServiceTest extends KernelTestCase
{
private function loggerMock()
{
return $this->createMock(LoggerInterface::class);
}
protected function setUp()
{
self::bootKernel();
}
/**
* #test
* #covers ::logName
*/
public function itShouldLogName()
{
// returns the real and unchanged service container
$container = self::$kernel->getContainer();
// gets the special container that allows fetching private services
$container = self::$container;
$loggerMock = $this->loggerMock();
$loggerMock->expect(self::once())
->method('log')
->with('info', 'Name of the service', ['name' => 'service_test']);
$this->logger = $container->set(LoggerInterface::class, $loggerMock);
$baseService = new BaseService('service_test');
var_dump($baseService->getLogger());
}
}
Is there a solution to test such a logger inside the service ?

You can override the service to be public (only for the test environment) in your config_test.yml as follows:
services:
Psr\Log\LoggerInterface:
public: true
This is commonly done for testing private services.

Related

Symfony 3: Global call for the Controller after user login

Because I have many controllers, after the user is logged in, I want to transfer some user settings to Twig. But I don't want to make it in each controller:
Eg:
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
public function indexAction()
{
$user = $this->getUser();
$settings = $user->getSettings();
// ...
}
}
Is there a possibility to make it at a higher level from the call of the DefaultController?
Solution 1
Use app.user variable in Twig which is globally accessible:
{{ app.user.username }}
More info: https://symfony.com/doc/current/templates.html#the-app-global-variable
Solution 2
Write custom Twig function:
// src/Twig/UserExtension.php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Symfony\Component\Security\Core\Security;
class UserExtension extends AbstractExtension
{
Security $security
public function __construct(Security $security)
{
// Avoid calling getUser() in the constructor: auth may not
// be complete yet. Instead, store the entire Security object.
$this->security = $security;
}
public function getFunctions()
{
return [
new TwigFunction('get_user_settings', [$this, 'getUserSettings']),
];
}
public function getUserSettings()
{
$user = $this->security->getUser();
return $user->getSettings();
}
}
Usage in Twig:
{{ get_user_settings().setting1 }}
More info: https://symfony.com/doc/current/templating/twig_extension.html
How to inject current logged user: https://symfony.com/doc/current/security.html#b-fetching-the-user-from-a-service
Ok, maybe I don't describe it clearly. This is the solution I have searched for:
<?php
namespace AppBundle\Subscriber;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class ControllerSubscriber implements EventSubscriberInterface
{
/**
* #var TokenStorageInterface
*/
private $tokenStorage;
/**
* #var EntityManagerInterface
*/
private $em;
/**
* ControllerSubscriber constructor.
*
* #param TokenStorageInterface $tokenStorage
* #param EntityManagerInterface $em
*/
public function __construct(TokenStorageInterface $tokenStorage, EntityManagerInterface $em)
{
$this->tokenStorage = $tokenStorage;
$this->em = $em;
}
/**
* #param FilterControllerEvent $event
*/
public function onKernelController(FilterControllerEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
if (!$token = $this->tokenStorage->getToken()) {
return;
}
if (!$user = $token->getUser()) {
return;
}
// if no user
if (!$user instanceof UserInterface) {
return;
} else {
// .... do something
}
}
/**
* #return array
*/
public static function getSubscribedEvents()
{
return array(
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::CONTROLLER => array(array('onKernelController', 1)),
);
}
}
services.yml
app.controller.subscriber:
class: AppBundle\Subscriber\ControllerSubscriber
arguments: ["#security.token_storage","#doctrine.orm.default_entity_manager"]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }

Symfony Flex: Overriding Twig's routing path function

In Symfony2 it was straightforward to override the RoutingExtension, that I could inject some extra parameters.
I'm using a dynamic domain to route to different parts of my application.
{subdomain}.domain.com
However, I don't want to have to specify subdomain every time I call path or url in twig.
I could create my own unique filter name, but I'd rather not.
Previously, we could put this in the services.yaml file and it would work.
services:
twig.extension.routing:
class: AppBundle\Twig\Extension\RoutingExtension
public: false
arguments:
- '#router'
- '#request_stack'
- '%domain%'
Symfony2 Twig overriding default path function
With Symfony Flex, all I get is Unable to register extension "App\TwigExtension\TwigRoutingExtension" as it is already registered.
This is how to override Twig's routing path/url functions in Symfony 5.x using AbstractExtension:
namespace App\Twig;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class MyRoutingExtension extends AbstractExtension
{
public function __construct(
private UrlGeneratorInterface $generator
){}
public function getFunctions(): array
{
return [
new TwigFunction('path', [$this, 'getPath']),
new TwigFunction('url', [$this, 'getUrl']),
];
}
public function getPath(string $name, array $parameters = [], bool $relative = false): string
{
return $this->generator->generate($name, $parameters, $relative ? UrlGeneratorInterface::RELATIVE_PATH : UrlGeneratorInterface::ABSOLUTE_PATH);
}
public function getUrl(string $name, array $parameters = [], bool $schemeRelative = false): string
{
return $this->generator->generate($name, $parameters, $schemeRelative ? UrlGeneratorInterface::NETWORK_PATH : UrlGeneratorInterface::ABSOLUTE_URL);
}
}
In SF4 it's even simpler:
first composer require symfony/twig-bundle twig/extensions
Normally autowiring is enable so you can simply do:
<?php
namespace App\Twig;
use Symfony\Bridge\Twig\Extension\RoutingExtension;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class TestExtension extends RoutingExtension
{
public function __construct(UrlGeneratorInterface $generator)
{
parent::__construct($generator);
}
public function getPath($name, $parameters = array(), $relative = false)
{
return parent::getPath($name, $parameters, $relative);
}
}
If you want to setup a service forget about arguments definition it's boring :). Assuming your %domain% is available in your parameter do something like this:
<?php
namespace App\Twig;
use Symfony\Bridge\Twig\Extension\RoutingExtension;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
class TestExtension extends RoutingExtension
{
/** #var RouterInterface $router */
protected $router;
/** #var RequestStack $stack */
protected $stack;
/** #var mixed $domain */
protected $domain;
/**
* TestExtension constructor.
*
* #param RouterInterface $router
* #param RequestStack $stack
* #param ParameterBagInterface $bag
*/
public function __construct(RouterInterface $router, RequestStack $stack, ParameterBagInterface $bag)
{
$this->router = $router;
$this->stack = $stack;
$this->domain = $bag->get('domain');
}
public function getPath($name, $parameters = array(), $relative = false)
{
return parent::getPath($name, $parameters, $relative);
}
}

Symfony mock service when test console command

How to mock some service when test console command. I have some console command, in this command I get some service and I want mock this service
console command
const APP_SATISFACTION_REPORT = 'app:satisfactionrepor';
protected function configure()
{
$this
->setName(self::APP_SATISFACTION_REPORT)
->setDescription('Send Satisfaction Report');
}
/**
* #param InputInterface $input
* #param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$container = $this->getContainer();
$serviceCompanyRepo = $container->get('app.repository.orm.service_company_repository');
$satisfactionReport = $container->get('app.services.satisfaction_report');
/** #var ServiceCompany $serviceCompany */
foreach ($serviceCompanyRepo->findAll() as $serviceCompany) {
try {
$satisfactionReport->sendReport($serviceCompany);
} catch (\Exception $e) {
$io->warning(sprintf(
'Failed to send satisfaction report for service company with ID %s',
$serviceCompany->getId()
));
}
}
}
and my tests
/** #var Console\Application $application */
protected $application;
protected $container;
/** #var BufferedOutput $output */
protected $output;
/**
* #var ServiceCompanyRepository
*/
private $serviceCompanyRepository;
prepare console command
public function setUp()
{
parent::setUp();
$entityManager = $this->getEntityManager();
$this->serviceCompanyRepository = $entityManager->getRepository(ServiceCompany::class);
static::bootKernel();
$this->container = static::$kernel->getContainer();
$this->application = new Console\Application(static::$kernel);
$this->application->setAutoExit(false);
$master = new SatisfactionReportCommand();
$this->application->add($master);
}
public function setUpMaster() {
$this->output = new BufferedOutput();
$this->application->run(new ArgvInput([
'./bin/console',
SatisfactionReportCommand::APP_SATISFACTION_REPORT,
]), $this->output);
}
public function testGetMasterOutput()
{
$this->loadFixture(ServiceCompany::class);
/** #var ServiceCompany[] $serviceCompanies */
$serviceCompanies = $this->serviceCompanyRepository->findAll();
$this->assertCount(2, $serviceCompanies);
$client = self::createClient();
mock service app.services.satisfaction_report
$service = $this->getMockService($serviceCompanies);
and set it in container
$client->getContainer()->set('app.services.satisfaction_report', $service);
$this->setUpMaster();
$output = $this->output->fetch();
}
protected function getMockService($serviceCompanies)
{
$service = $this->getMockBuilder(SatisfactionReport::class)
->setMethods(['sendReport'])
->disableOriginalConstructor()
->getMock();
$service
->expects($this->exactly(2))
->method('sendReport')
->withConsecutive(
[$serviceCompanies[0]],
[$serviceCompanies[1]]
);
return $service;
}
How to mock app.services.satisfaction_report? Set in container app.services.satisfaction_report not help me
I had same problem but I resolved it.
I have base class:
class TestCase extends WebTestCase
{
/** #var Application */
private $application;
private $mailServiceMock;
protected function setMailService(MailService $mailServiceMock): void
{
$this->mailServiceMock = $mailServiceMock;
}
protected function getApplication(): Application
{
static::bootKernel();
static::$kernel->getContainer()->get('test.client');
$this->setMocks();
$this->application = new Application(static::$kernel);
$this->application->setCatchExceptions(false);
$this->application->setAutoExit(false);
return $this->application;
}
protected function execute(string $action, array $arguments = [], array $inputs = []): CommandTester
{
$tester = (new CommandTester($this->getApplication()->find($action)))->setInputs($inputs);
$tester->execute($arguments);
return $tester;
}
private function setMocks(): void
{
if ($this->mailServiceMock) {
static::$kernel->getContainer()->set('mail', $this->mailServiceMock);
}
}
}
And test class
class SendEmailCommandTest extends TestCase
{
public function testExecuteSendingError(): void
{
$mailServiceMock = $this->getMockBuilder(MailService::class)->disableOriginalConstructor()
->setMethods(['sendEmail'])->getMock();
$mailServiceMock->method('sendEmail')->willThrowException(new \Exception());
$this->setMailService($mailServiceMock);
$tester = $this->execute(SendEmailCommand::COMMAND_NAME, self::NORMAL_PAYLOAD);
$this->assertEquals(SendEmailCommand::STATUS_CODE_EMAIL_SENDING_ERROR, $tester->getStatusCode());
}
}
As you can see I set mail service right after booting kernel.
And here you can see my services.yaml:
services:
mail.command.send.email:
autowire: true
class: App\Command\SendEmailCommand
arguments: ["#logger", "#mail"]
tags:
- {name: console.command, command: "mail:send.email"}
If you create the commands as a service, where the framework injects the services automatically (either autowired, or with an explicit argument list) into a constructor (tip: in the command, call the parent::__construct()), then the test can create whatever mock or other replacement service that matches the parameter typehint (or interface).
here is my example class:
class MainCommandTest extends IntegrationTestCase
{
/**
* #var MainCommand
*/
protected $subject;
/**
* #var Application
*/
protected $application;
/**
* sets test subject
*
* #return void
*/
public function setUp()
{
parent::setUp();
static::bootKernel();
$readStreams = new ReadStreams();
$udpStreamMock = $this->getMockBuilder(UdpStream::class)->disableOriginalConstructor()->setMethods(['readIncomingStreams'])->getMock();
$udpStreamMock->expects($this->once())->method('readIncomingStreams')->willReturn($readStreams);
static::$kernel->getContainer()->set(UdpStream::class, $udpStreamMock);
$application = new Application($this::$kernel);
$this->subject = $this->getService(MainCommand::class);
$application->add( $this->subject);
$this->application = $application;
}
/**
* Tests command in $subject command,
*
* #return void
*/
public function testCommand()
{
$command = $this->application->find( $this->subject->getName());
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'command' => $this->subject->getName()
]
);
$this->stringContains($commandTester->getDisplay(true), 'finished');
$this->assertEquals($commandTester->getStatusCode(), 0);
}
}

Kernel boot error-symfony netbeans

I have the following code and I just need to test my controller and it takes a request as parameter. But keep getting
Warning: Deprecated JSON test listener used
and
PayrollperiodControllerTest::testPayrollWeekCreateAction
Error: Call to undefined method PayrollperiodControllerTest::bootKernel()
class PayrollperiodControllerTest extends PHPUnit_Framework_TestCase {
/**
* #var \Doctrine\ORM\EntityManager
*/
private $em;
/**
* {#inheritDoc}
*/
public function setUp()
{
self::bootKernel();
$this->em = static::$kernel->getContainer()
->get('doctrine')
->getManager();
}
public function testPayrollWeekCreateAction(Request $request) {
$request = Request::create('http://localhost:8000/web/app_dev.php/payrollperiod/new', 'GET');
$result=$this->em->handle($request);
$this->assertTrue($result->isSuccessful);
}
/**
* {#inheritDoc}
*/
public function tearDown() {
parent::tearDown();
$this->em->close();
$this->em = null; //avoid memory Leaks
}
}
How can I fix these errors?
You should extend the KernelTestCase instead of PHPUnit_Framework_TestCase, so try this:
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
...
class PayrollperiodControllerTest extends KernelTestCase {
instead of this:
class PayrollperiodControllerTest extends PHPUnit_Framework_TestCase {
Hope this help

Symfony2 - Functional Testing: container not injected into a Doctrine DataFixture - Call to a member function get() on a non-object

Attempting to build a user data fixture results in
Fatal error: Call to a member function get() on a non-object in
...\Tests\Repository\DataFixtures\LoadAdminUserData.php on line 35
line 35:
$discriminator = $this->container->get('pugx_user.manager.user_discriminator');
Complete fixture (w/ namespace edit)
<?php
//src\Vol\VolBundle\\DataFixtures\ORM\LoadAdminUserData
namespace Vol\VolBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Description of LoadAdminUserData
*
*/
class LoadAdminUserData extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
{
/**
* #var ContainerInterface
*/
private $container;
/**
* {#inheritDoc}
*/
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
public function load(ObjectManager $manager)
{
$discriminator = $this->container->get('pugx_user.manager.user_discriminator');
$discriminator->setClass('Vol\VolBundle\Entity\Admin');
$userManager = $this->container->get('pugx_user_manager');
$admin = $userManager->createUser();
$admin->setUsername('bborko');
$admin->setEmail('bborko#bogus.info');
$admin->setPlainPassword('123456');
$admin->setEnabled(true);
$userManager->updateUser($admin, true);
$this->addReference('test-user', $admin);
}
public function getOrder()
{
return 3;
}
}
Test
<?php
//src\Vol\VolBundle\Tests\Repository
namespace Vol\VolBundle\Tests\Repository;
use Vol\VolBundle\DataFixtures\ORM\DoctrineTestCase;
/**
* Description of AdminUserRepositoryTest
*
* #author George
*/
class AdminUserRepositoryTest extends DoctrineTestCase
{
/**
* Set up repository test
*/
public function setUp()
{
$this->loadFixturesFromDirectory($this->dir);
}
/**
* Test finding all countries ordered
*/
public function testFindAll()
{
$focuses = $this->getRepository()->findAll();
$this->assertCount(1, $focuses, 'Should return 1 admin user');
}
/**
* Returns repository
*
* #return \Vol\VolBundle\Entity\Admin
*/
protected function getRepository()
{
return $this->em->getRepository('\Vol\VolBundle\Entity\Admin');
}
}
class DoctrineTestCase
<?php
//src\Vol\VolBundle\Tests\Repository
namespace Vol\VolBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Loader;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* Class DoctrineTestCase
*
* This is the base class to load doctrine fixtures using the symfony configuration
*/
class DoctrineTestCase extends WebTestCase
{
/**
* #var \Symfony\Component\DependencyInjection\Container
*/
protected $container;
/**
* #var \Doctrine\ORM\EntityManager
*/
protected $em;
/**
* #var string
*/
protected $environment = 'test';
/**
* #var bool
*/
protected $debug = true;
/**
* #var string
*/
protected $entityManagerServiceId = 'doctrine.orm.entity_manager';
protected $dir;
/**
* Constructor
*
* #param string|null $name Test name
* #param array $data Test data
* #param string $dataName Data name
*/
public function __construct($name = null, array $data = array(), $dataName = '')
{
parent::__construct($name, $data, $dataName);
if (!static::$kernel) {
static::$kernel = self::createKernel(array(
'environment' => $this->environment,
'debug' => $this->debug
));
static::$kernel->boot();
}
$this->container = static::$kernel->getContainer();
$this->em = $this->getEntityManager();
$this->dir = __DIR__;
}
/**
* Executes fixtures
*
* #param \Doctrine\Common\DataFixtures\Loader $loader
*/
protected function executeFixtures(Loader $loader)
{
$purger = new ORMPurger();
$executor = new ORMExecutor($this->em, $purger);
$executor->execute($loader->getFixtures());
}
/**
* Load and execute fixtures from a directory
*
* #param string $directory
*/
protected function loadFixturesFromDirectory($directory)
{
$loader = new Loader();
$loader->loadFromDirectory($directory);
$this->executeFixtures($loader);
}
/**
* Returns the doctrine orm entity manager
*
* #return object
*/
protected function getEntityManager()
{
return $this->container->get($this->entityManagerServiceId);
}
}
Solution:
You'll have to pass the container to your fixture-loader inside the TestCase yourself.
Just use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader ...
... which extends Doctrine\Common\DataFixtures\Loader and expects the container ...
... as constructor argument.
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader;
protected function loadFixturesFromDirectory($directory)
{
$loader = new ContainerAwareLoader($this->container);
$loader->loadFromDirectory($directory);
$this->executeFixtures($loader);
}
When does symfony2 inject the container automatically?
The FrameworkBundle injects the container into instances of ContainerAwareInterface being controllers or commands only.
The corresponding code can be found here and here.
DataFixtures can be container-aware, too.
DoctrineFixtureBundle's Command searches for (container-aware) fixtures and injects the container.
Only fixtures that conventially live in a bundle's DataFixtures/ORM folder are processed.
A quick look at the code reveals it:
foreach ($this->getApplication()->getKernel()->getBundles() as $bundle) {
$paths[] = $bundle->getPath().'/DataFixtures/ORM';
}
The --fixtures flag:
Appending the --fixtures flag - which can be a string or an array - to the doctrine:fixtures:load allows to add additional fixture-paths for processing.

Resources