How to set parameters or services in Symfony in PHPUnit tests? - phpunit

We are using PHPUnit to test parts of our application. In some tests, we want to change the value of a parameter or override a service (but only for that test, not for all tests).
What is the recommended way to configure Symfony's container on the fly in tests?
The problem we have met is that the container doesn't recompile itself when config is set on the fly (because it only recompiles itself when files have changed).

This is how we proceed for now:
class TestKernel extends \AppKernel
{
public function __construct(\Closure $containerConfigurator, $environment = 'test', $debug = false)
{
$this->containerConfigurator = $containerConfigurator;
parent::__construct($environment, $debug);
}
public function registerContainerConfiguration(LoaderInterface $loader)
{
parent::registerContainerConfiguration($loader);
$loader->load($this->containerConfigurator);
}
/**
* Override the parent method to force recompiling the container.
* For performance reasons the container is also not dumped to disk.
*/
protected function initializeContainer()
{
$this->container = $this->buildContainer();
$this->container->compile();
$this->container->set('kernel', $this);
}
}
Then we have added this method in our PHPUnit base test class:
/**
* Rebuilds the container with custom container configuration.
*
* #param \Closure $containerConfigurator Closure that takes the ContainerBuilder and configures it.
*
* Example:
*
* $this->rebuildContainer(function (ContainerBuilder $container) {
* $container->setParameter('foo', 'bar');
* });
*/
public function rebuildContainer(\Closure $containerConfigurator) : ContainerInterface
{
if ($this->kernel) {
$this->kernel->shutdown();
$this->kernel = null;
$this->container = null;
}
$this->kernel = new TestKernel($containerConfigurator);
$this->kernel->boot();
$this->container = $this->kernel->getContainer();
return $this->container;
}

As of Symfony 5, there is a possible shortcut. As long as all you need is to set parameters, you can simply change them in the $_ENV variable. I.e. the following works
services.yaml
services:
App\Controller\ApiController:
arguments:
$param: '%env(my_param)%'
ApiController.php
class ApiController extends AbstractController
{
public function __construct(string $param)
{
$this->param = $param;
}
...
}
test
class ApiControllerTest extends WebTestCase
{
public function testSomething(): void
{
$_ENV['param'] = 'my-value';
$client = self::createClient();
$client->request(...)
}
...
}

You do not test the container or config in Unit Tests.
In Unit Tests the goal is to test the units encapsulated without the full application stack.
For functional tests the recommended way is to edit it in the inherited config under app/config/config_test.yml
All values from config_dev.yml can be overridden there.

Related

Get rootDir inside beforeSend function in Symfony to intercept Sentry event

I have a service who intercepts the events of Sentry. I'm using a function called beforeSend.
I would to load a json file who contains the data to scrub or to keep. It's a service and I build my constructor with a similar way than others, but the "$this" context doesn't exist when I'm in the debugger in this function.
The kernel is in the Global variables, but I think it's not a good idea... I only would to get the root dir and it's all, but I don't find how to do this in this class... The constructor seems useless.
Someone could help me with a similar experience ?
EDIT :
Service :
namespace App\Services;
use Sentry\Event;
use Symfony\Component\HttpKernel\KernelInterface;
class SentryBeforeSendService
{
private static $rootDir;
public function __construct(KernelInterface $kernel)
{
self::$rootDir = $kernel->getRootDir();
}
/**
* Scrubs the value of all TARGET_PARAMETERS
* in the event's request.
*
* #param Event $event
*
* #return Event
*/
public function beforeSend(Event $event)
{
$rootDir = self::$rootDir;
$event->setRequest(self::scrubRequest($event->getRequest(), $rootDir));
try {
$composerData = json_decode(file_get_contents($rootDir.'/../composer.json'), true);
$version = $composerData['version'];
$event->setRelease($version);
} catch (\Exception $e) {
//do nothing
}
return $event;
}
/**
* Scrubs GET and POST parameters
*
* #param array $request
*
* #return array
*/
private static function scrubRequest(array $request, $rootDir)
{
// DO SOMETHING WITH $rootDir to scrub data with external file
}}
services.yml :
app.service.sentry_before_send:
class: 'App\Services\SentryBeforeSendService'
arguments: ['#kernel']
config_prod.yml :
sentry:
dsn: "%sentry_dsn%"
options:
environment: "%sentry_environment%"
# release: '%env(VERSION)%' #overridden from composer.json version in SentryBeforeSendService::beforeSend
before_send: 'App\Services\SentryBeforeSendService::beforeSend'
But it seems the construct never happened.
Thank you very much.
I was unable to inject a parameter, but I found a way to get the project_root from my method. Half victory ...
config_prod.yml:
sentry:
dsn: "%sentry_dsn%"
options:
environment: "%sentry_environment%"
# release: '%env(VERSION)%' #overridden from composer.json version in SentryBeforeSendService::beforeSend
before_send: 'App\Services\SentryBeforeSendService::beforeSend'
project_root: '%kernel.project_dir%'
Service :
<?php
namespace App\Services;
use Sentry\Event;
use Sentry\State\Hub;
class SentryBeforeSendService
{
private static $projectRoot;
/**
* Scrubs the value of all TARGET_PARAMETERS
* in the event's request.
*
* #param Event $event
*
* #return Event
*/
public function beforeSend(Event $event)
{
$sentryClient = Hub::getCurrent()->getClient();
self::$projectRoot = $sentryClient->getOptions()->getProjectRoot();
$event->setRequest(self::scrubRequest($event->getRequest()));
try {
$composerData = json_decode(file_get_contents(self::$projectRoot.'/composer.json'), true);
$version = $composerData['version'];
$event->setRelease($version);
} catch (\Exception $e) {
//do nothing
}
return $event;
}}
Hope it'll help someone else.
Thank you for answers.
You can inject the kernel.project_dir parameter in your service constructor with a named parameter:
In your services.yml file:
services:
_defaults:
bind:
string $kernelProjectDir: '%kernel.project_dir%'
Then in your service:
public function __construct(string $kernelProjectDir)
{

Testion controler method in symfony (phpUnit)

i need some help i want to write a unit test about a controler method , i have searched for examples and tested a lot of method's but none of them has worked:
Here is my controller:
class ComputerController extends Controller
{
/**
* #Route("/list-computers.html", name="back_computer_list")
* #return RedirectResponse|Response
*/
function listComputerAction()
{
$ad = $this->get("ldap_service");
$computers = $ad->getAllComputer();
return $this->render('BackBundle:Computer:list.html.twig', array(
"computers" => $computers,
));
}
I have tried to test it with mock like this:
class ComputerController extends Controller
{
/**
* #var EngineInterface
*/
private $templating;
public function setTemplating($templating)
{
$this->templating = $templating;
}
and i have created a test method:
class ComputerControllerTest extends TestCase {
public function testlistComputerAction(){
$templating = $this->getMockBuilder('BackBundle\Controller\ComputerController')->getMock();
$computers = [1,2];
$templating->expects($this->once())
->method('render')
->with('BackBundle:Computer:list.html.twig', array(
"computers" => $computers))
->will($this->returnValue( $computers));
$controller = new ComputerController();
$controller->setTemplating($templating);
$this->assertEquals('success', $controller->listComputerAction());
}
When i start executing phpunit , i have this warning"Trying to configure method "render" which cannot be configured because it does not exist, has not been specified, is final, or is static"
I would be thankful if someone has an idea about this
I tried to Test a method in ldapService : Here is the method's of the service that i want to test
/**
* #return bool|resource
*/
public function getLdapBind()
{
if (!$this->ldapBind) {
if ($this->getLdapConnect()) {
$this->ldapBind = #ldap_bind($this->ldapConnect, $this->ldapUser, $this->ldapPass);
}
}
return $this->ldapBind;
}
/**
* #param $ldapUser
* #param $password
* #return bool
*/
function isAuthorized($ldapUser, $password)
{
$result = false;
if ($this->ldapConnect) {
$result = #ldap_bind($this->ldapConnect, $ldapUser, $password);
}
return $result;
}
Here is the test (using Mock):
<?php
namespace BackBundle\Tests\Service;
use PHPUnit\Framework\TestCase;
use BackBundle\Service\LdapService;
use PHPUnit_Framework_MockObject_InvocationMocker;
class LdapServiceTest extends TestCase {
public function testgetLdapConnect()
{
// $LdapService = new LdapService();
$ldapMock = $this->getMockBuilder( 'LdapService')->setMethods(['getLdapBind'])->disableOriginalConstructor()->getMock();
$ldapMock->expects($this->once())
// ->method()
->with(array('ldap_bind', 'mike', 'password'))
->will($this->returnValue(true));
$ldapMock->isAuthorized('mike', 'password');
}
}
But i have a warning that i can't resolve : "Method name matcher is not defined, cannot define parameter matcher without one"
If someone , has an idea about that please
Honestly, there is nothing useful to test in that three-line controller. #1 is the service container, and #3 is the Twig subsystem. Line #2 can be unit tested on it's own.
With more complex controllers, I have found that making them a service where all the dependencies are passed in, either by constructor, or into the action itself does make slightly more complex controllers quite easy, but very few need that anyway.

How to load Symfony2 fixtures from migration class?

We've built up a set of data fixtures to seed the database with all our reference values. We are also using the DoctrineMigrationsBundle to manage schema updates. We would like to trigger the fixture load within our initial schema migration class so the system gets populated before running any additional schema updates.
I found in the docs that you can make migration classes container aware, but I can't figure out how to jump from that to calling/running the data fixtures. I haven't found any good answers on Stackoverflow or via google. Has anyone done this and can point me in the right direction? (or have suggestions on a better way to manage seed data in conjunction with schema migrations). Thanks.
This is using Symfony Version: 2.4
This is interesting question. I've found the "dirty" solution, but it works well.
namespace Application\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
class Version20140811164659 extends AbstractMigration implements ContainerAwareInterface
{
private $container;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
public function up(Schema $schema)
{
// ... your code here
}
public function postUp(Schema $schema)
{
// here you have to define fixtures dir
$this->loadFixtures('src/Acme/BlogBundle/DataFixtures/ORM');
}
public function down(Schema $schema)
{
// ... your code here
}
public function loadFixtures($dir, $append = true)
{
$kernel = $this->container->get('kernel');
$application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel);
$application->setAutoExit(false);
//Loading Fixtures
$options = array('command' => 'doctrine:fixtures:load', "--fixtures" => $dir, "--append" => (boolean) $append);
$application->run(new \Symfony\Component\Console\Input\ArrayInput($options));
}
}
This solution simply running console command php app/console doctrine:fixtures:load --fixtures=src/Acme/BlogBundle/DataFixtures/ORM --append after "up" migration.
Sorry for poore English. If you'll find clear solution, share it ;)
I've made a migration class to address this very problem. The code is essentially inspired from the doctrine:fixtures:load command.
<?php
namespace AppBundle\Migrations;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
abstract class AbstractFixturesAwareMigration extends AbstractMigration implements ContainerAwareInterface
{
private $container;
private $fixtures;
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
protected function getContainer()
{
return $this->container;
}
protected function addFixture(FixtureInterface $fixture)
{
if(null === $this->fixtures) {
$this->fixtures = new ContainerAwareLoader($this->getContainer());
}
$this->fixtures->addFixture($fixture);
return $this;
}
protected function executeFixtures($em = null, $append = true, $purgeMode = ORMPurger::PURGE_MODE_DELETE)
{
$em = $this->getContainer()->get('doctrine')->getManager($em);
$purger = new ORMPurger($em);
$purger->setPurgeMode($purgeMode);
$executor = new ORMExecutor($em, $purger);
$executor->execute($this->fixtures->getFixtures(), $append);
$this->fixtures = null;
return $this;
}
}
Usage is pretty straightforward:
<?php
namespace Application\Migrations;
use AppBundle\Migrations\AbstractFixturesAwareMigration
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20170726102103 extends AbstractFixturesAwareMigration
{
/**
* #param Schema $schema
*/
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
// [...]
}
public function postUp(Schema $schema)
{
// LoadMyData can be any fixture class
$this->addFixture(new LoadMyData());
$this->executeFixtures();
}
/**
* #param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
// [...]
}
}

Too many connection during unit testing

I have a project with a lot of tests class like
class MyTest extends BaseTestCase
{
public function __construct()
{
parent::__construct();
$this->em = $this->get('doctrine')->getManager();
}
public function setUp() {
$this->init();
//load sql data for the tests
$path = $this->get('kernel')->locateResource('#Bundle/Data/Test.sql');
$content_file_sql_data = file_get_contents($path);
$stmt = $this->em->getConnection()->prepare($content_file_sql_data);
$stmt->execute();
$stmt->closeCursor();
}
/*
* Then we do a lot of tests using the database
*/
}
They all extends my BaseTestCase:
abstract class BaseTestCase extends \PHPUnit_Framework_TestCase {
protected $_container;
protected $kernel;
public function __construct() {
parent::__construct();
$this->kernel = new \AppKernel("test", true);
$this->kernel->boot();
$this->_container = $this->kernel->getContainer();
$this->init();
}
//empty the database before each test class
public function init() {
$this->_application = new Application($this->kernel);
$this->_application->setAutoExit(false);
//rebuild and empty the database
$this->runConsole("doctrine:schema:drop", array("--force" => true));
$this->runConsole("doctrine:schema:create");
}
Since I have a lot of tests, i have recently got some errors PDOException: SQLSTATE[08004] [1040] Too many connections. It's like phpunit never close database connection, and around 100 tests I get this error for all the other tests.
How can i fix it?
I tried to put a last test doing $this->em->close() at the end of each test class but it didn't solve it
Some additional information: i'm pretty sure I don't have an issue with ONE test, because if I change the order of the test suite, the error appears around the same amount of tests class passed
My solution was to override shutdown method in my Bundle class:
public function shutdown()
{
if ('test' == $this->container->getParameter('kernel.environment')) {
/* #var EntityManager $em */
$em = $this->container->get('doctrine.orm.default_entity_manager');
$em->getConnection()->close();
}
}

symfony 2 Updating color schemes dynamically

I'm working in a project using Symfony 2,
I'm using Assetic with rewrite and less filter, and it work fine,
Now I'm planing to let administrator (connected user) to controle some features in css like font and main color.
The problem that I'm facing is :
- how can I proceed to integrate these css changes from entity to the css management
I can't let assetic use routing rule to include custom css
Eaven if I success to get this work, every time I have a changes to the custom css I have to install assets to web folder and make the assetic:dump and clearing cache from a controller.
If you (or someone else) still need this:
I solved this by putting all generic CSS in a asset handled by Assetic like usual and putting the dynamic CSS generation in a Controller action and rendering the CSS with Twig.
As suggested by Steffen you should put the dynamic CSS in a Twig template.
But now you might suffer from that part of the css being a full request to a symfony application instead of a css (HTTP 302 and such) which increases server load.
Thats why I would advise you to do 3 things (you can skip step 2 if your css doesn't change without interaction, e.g. date based):
Implement a service which caches the current output to e.g. web/additional.css.
Write and register a RequestListener to update the css regularly
Extend all controller actions that could introduce changes to the css with the service call
Example (assumes you use Doctrine and have an entity with some color information):
Service
<?php
//Acme\DemoBundle\Service\CSSDeployer.php
namespace Acme\DemoBundle\Service;
use Doctrine\ORM\EntityManager;
class CSSDeployer
{
/**
* #var EntityManager
*/
protected $em;
/**
* Twig Templating Service
*/
protected $templating;
public function __construct(EntityManager $em, $templating)
{
$this->em = $em;
$this->templating = $templating;
}
public function deployStyle($filepath)
{
$entity = $this->em->getRepository('AcmeDemoBundle:Color')->findBy(/* your own logic here */);
if(!$entity) {
// your error handling
}
if(!file_exists($filepath)) {
// your error handling, be aware of the case where this service is run the first time though
}
$content = $this->templating->render('AcmeDemoBundle:CSS:additional.css.twig', array(
'data' => $entity
));
//Maybe you need to wrap below in a try-catch block
file_put_contents($filepath, $content);
}
}
Service Registration
#Acme\DemoBundle\Resources\config\services.yml
services:
#...
css_deployer:
class: Acme\DemoBundle\Service\CSSDeployer
arguments: [ #doctrine.orm.entity_manager, #templating ]
RequestListener
<?php
//Acme\DemoBundle\EventListener\RequestListener.php
namespace Acme\DemoBundle\EventListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Debug\Exception\ContextErrorException;
use \DateTime;
use Doctrine\ORM\EntityManager;
class RequestListener
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* #var EntityManager
*/
protected $em;
public function __construct(ContainerInterface $container, $em)
{
$this->container = $container;
$this->em = $em;
}
/**
* Checks filemtime (File modification time) of web/additional.css
* If it is not from today it will be redeployed.
*/
public function onKernelRequest(GetResponseEvent $event)
{
$kernel = $event->getKernel();
$container = $this->container;
$path = $container->get('kernel')->getRootDir().'/../web'.'/additional.css';
$time = 1300000000;
try {
$time = #filemtime($path);
} catch(ContextErrorException $ex) {
//Ignore
} catch(\Exception $ex) {
//will never get here
if(in_array($container->getParameter("kernel.environment"), array("dev","test"))) {
throw $ex;
}
}
if($time === FALSE || $time == 1300000000) {
file_put_contents($path, "/*Leer*/");
$time = 1300000000;
}
$modified = new \DateTime();
$modified->setTimestamp($time);
$today = new \DateTime();
if($modified->format("Y-m-d")!= $today->format("Y-m-d")) {
//UPDATE CSS
try {
$container->get('css_deployer')->deployStyle($path);
} catch(\Exception $ex) {
if(in_array($container->getParameter("kernel.environment"), array("dev","test"))){
throw $ex;
}
}
} else {
//DO NOTHING
}
}
}
RequestListener registration
#Acme\DemoBundle\Resources\config\services.yml
acme_style_update_listener.request:
class: Acme\DemoBundle\EventListener\RequestListener
arguments: [ #service_container, #doctrine.orm.entity_manager ]
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Controller actions
public function updateAction()
{
// Stuff
$path = '....';
$this->get('css_deployer')->deployStyle($path);
}
Hope this helps someone in the future.

Resources