I'm trying to unit test a controller, but can't figure out how to pass some extra parameters to the routeMatch object.
I followed the posts from tomoram at http://devblog.x2k.co.uk/unit-testing-a-zend-framework-2-controller/ and http://devblog.x2k.co.uk/getting-the-servicemanager-into-the-test-environment-and-dependency-injection/, but when I try to dispatch a request to /album/edit/1, for instance, it throws the following exception:
Zend\Mvc\Exception\DomainException: Url plugin requires that controller event compose a router; none found
Here is my PHPUnit Bootstrap:
class Bootstrap
{
static $serviceManager;
static $di;
static public function go()
{
include 'init_autoloader.php';
$config = include 'config/application.config.php';
// append some testing configuration
$config['module_listener_options']['config_static_paths'] = array(getcwd() . '/config/test.config.php');
// append some module-specific testing configuration
if (file_exists(__DIR__ . '/config/test.config.php')) {
$moduleConfig = include __DIR__ . '/config/test.config.php';
array_unshift($config['module_listener_options']['config_static_paths'], $moduleConfig);
}
$serviceManager = Application::init($config)->getServiceManager();
self::$serviceManager = $serviceManager;
// Setup Di
$di = new Di();
$di->instanceManager()->addTypePreference('Zend\ServiceManager\ServiceLocatorInterface', 'Zend\ServiceManager\ServiceManager');
$di->instanceManager()->addTypePreference('Zend\EventManager\EventManagerInterface', 'Zend\EventManager\EventManager');
$di->instanceManager()->addTypePreference('Zend\EventManager\SharedEventManagerInterface', 'Zend\EventManager\SharedEventManager');
self::$di = $di;
}
static public function getServiceManager()
{
return self::$serviceManager;
}
static public function getDi()
{
return self::$di;
}
}
Bootstrap::go();
Basically, we are creating a Zend\Mvc\Application environment.
My PHPUnit_Framework_TestCase is enclosed in a custom class, which goes like this:
abstract class ControllerTestCase extends TestCase
{
/**
* The ActionController we are testing
*
* #var Zend\Mvc\Controller\AbstractActionController
*/
protected $controller;
/**
* A request object
*
* #var Zend\Http\Request
*/
protected $request;
/**
* A response object
*
* #var Zend\Http\Response
*/
protected $response;
/**
* The matched route for the controller
*
* #var Zend\Mvc\Router\RouteMatch
*/
protected $routeMatch;
/**
* An MVC event to be assigned to the controller
*
* #var Zend\Mvc\MvcEvent
*/
protected $event;
/**
* The Controller fully qualified domain name, so each ControllerTestCase can create an instance
* of the tested controller
*
* #var string
*/
protected $controllerFQDN;
/**
* The route to the controller, as defined in the configuration files
*
* #var string
*/
protected $controllerRoute;
public function setup()
{
parent::setup();
$di = \Bootstrap::getDi();
// Create a Controller and set some properties
$this->controller = $di->newInstance($this->controllerFQDN);
$this->request = new Request();
$this->routeMatch = new RouteMatch(array('controller' => $this->controllerRoute));
$this->event = new MvcEvent();
$this->event->setRouteMatch($this->routeMatch);
$this->controller->setEvent($this->event);
$this->controller->setServiceLocator(\Bootstrap::getServiceManager());
}
public function tearDown()
{
parent::tearDown();
unset($this->controller);
unset($this->request);
unset($this->routeMatch);
unset($this->event);
}
}
And we create a Controller instance and a Request with a RouteMatch.
The code for the test:
public function testEditActionWithGetRequest()
{
// Dispatch the edit action
$this->routeMatch->setParam('action', 'edit');
$this->routeMatch->setParam('id', $album->id);
$result = $this->controller->dispatch($this->request, $this->response);
// rest of the code isn't executed
}
I'm not sure what I'm missing here. Can it be any configuration for the testing bootstrap? Or should I pass the parameters in some other way? Or am I forgetting to instantiate something?
What I did to solve this problem was move the Application::init() and the configuration from the Bootstrap to the setUp() method. It takes a while to load, but it works.
My Bootstrap has only the code needed to configure the autoloader, while the setUp() method has something similar to the old Bootstrap::go() code.
Related
I am trying to inject an service object into my Repository. I have created different Service Classes under the directory Classes/Services. There is also one class that I created called ContainerService, which creates and instantiate one ServiceObject for each Service Class.
ContainerService Class:
namespace VendorName\MyExt\Service;
use VendorName\MyExt\Service\RestClientService;
class ContainerService {
private $restClient;
private $otherService;
/**
* #return RestClientService
*/
public function getRestClient() {
$objectManager = GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
if ($this->restClient === null) {
$this->restClient = $objectManager->get(RestClientService::class);
}
return $this->restClient;
}
...
As I said, I create my ServiceObjects in the ContainerService Class.
Now I want to inject the ContainerService into my Repository and use it.
MyRepository Class:
namespace VendorName\MyExt\Domain\Repository;
use VendorName\MyExt\Service\ContainerService;
class MyRepository extends Repository
{
/**
* #var ContainerService
*/
public $containerService;
/**
* inject the ContainerService
*
* #param ContainerService $containerService
* #return void
*/
public function injectContainerService(ContainerService $containerService) {
$this->containerService = $containerService;
}
// Use Objects from The ContainerService
public function findAddress($addressId) {
$url = 'Person/getAddressbyId/'
$someData = $this->containerService->getRestClient()->sendRequest($url)
return $someData;
}
In MyController I recieve the $someData from my findAddress function and do some work with it.
But when I call my Page, I get following ErrorMessage:
(1/2) #1278450972 TYPO3\CMS\Extbase\Reflection\Exception\UnknownClassException
Class ContainerService does not exist. Reflection failed.
Already tried to reload all Caches and dumping the Autoload didn't help either.
Didn't install TYPO3 with composer.
I appreciate any advice or help! Thanks!
Actually found the Issue.
In MyRepository Class there was a Problem with the Annotations and the TypeHint:
namespace VendorName\MyExt\Domain\Repository;
use VendorName\MyExt\Service\ContainerService;
class MyRepository extends Repository
{
/**
*** #var \VendorName\MyExt\Service\ContainerService**
*/
public $containerService;
/**
* inject the ContainerService
*
* #param \VendorName\MyExt\Service\ContainerService $containerService
* #return void
*/
public function injectContainerService(\VendorName\MyExt\Service\ContainerService $containerService) {
$this->containerService = $containerService;
}
// Use Objects from The ContainerService
public function findAddress($addressId) {
$url = 'Person/getAddressbyId/'
$someData = $this->containerService->getRestClient()->sendRequest($url)
return $someData;
}
Now it works.
I have the following phpunit test for testing the controllers:
The DefaultControllerTest:
namespace Tests\AppBundle\Controller;
use Tests\AppBundle\Controller\BasicHttpController;
use AppBundle\DataFixtures\Test\DummyUserFixtures;
/**
* #testtype Functional
*/
class DefaultControllerTest extends BasicHttpController
{
/**
* {#inheritdoc}
*/
public function setUp()
{
$fixture = new DummyUserFixtures();
$fixture->load($this->entityManager);
}
/**
* Testing the Behavior when visiting the index page
*/
public function testIndex()
{
$client = $this->client;
$router=$client->getContainer()->get('router');
$crawler = $client->request('GET', '/');
$response=$client->getResponse();
$this->assertTrue($client->getResponse()->isRedirect());
$this->assertEquals($router->getRouteCollection()->get('fos_user_security_login')->getPath(),$response->headers->get('Location'));
//#todo Create Dummy Users
// $this->checkPanelAfterSucessfullLogin($crawler);
}
}
That extends the following test BasicHttpController (try to apply the DRY principle):
namespace Tests\AppBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
class BasicHttpController extends WebTestCase
{
protected $entityManager=null;
protected $client=null;
/**
* {#inheritdoc}
*/
public function __construct()
{
parent::__construct();
$this->client = static::createClient();
$container = $this->client->getContainer();
$doctrine = $container->get('doctrine');
$this->entityManager=$doctrine->getManager();
}
/**
* Remove all entities from the database
*/
protected function truncateEntities()
{
$purger = new ORMPurger($this->entityManager());
$purger->purge();
}
/**
* {#inheritdoc}
*/
public function tearDown()
{
$this->truncateEntities();
}
/**
* #param username String the user's username
* #param passwoρd String the user's password
*/
protected function checkPanelAfterSucessfullLogin($crawler,string $username,string $password)
{
//Submitting the form
$form=$crawler->selectButton('_submit')->form();
$form['_username']=$username;
$form['_password']=$password;
$crawler=$crawler->submit($form);
$response=$client->getResponse();
$this->assertTrue($client->getResponse()->isRedirect());
$client->followRedirect();
//Checking header
$headerDom=$crawler->filter('header')->childen()->filter('nav.navbar')->children();
$this->assertCount(1,$headerDom->find('a.navbar-brand')); //homepage link
$this->assertCount(1,$headerDom->find('a.btn-danger')); //Logout button
}
}
As you can see I try to load the following fixture:
namespace AppBundle\DataFixtures\Test;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\Common\Persistence\ObjectManager;
class DummyUserFixtures extends AbstractFixture implements OrderedFixtureInterface,ContainerAwareInterface
{
/**
* #var ContainerInterface
*/
private $container=null;
/**
* {#inheritDoc}
*/
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
}
/**
* Generic function that creates a user with provided information.
* #param $name {String} The user's name
* #param $surname {String} The user's surname
* #param $username {String} The user's username
* #param $password {String} The user's password
* #param $email {String} The user's recovery email
* #param $role {String} The user's system role
* #param $phone {String | null} The user's phone number
* #param $organization {String|null} The user's organization
* #param $occupation {String|null} The user's occupation
*
* #return AppBundle\Entity\User
*/
private function createUser($name,$surname,$username,$password,$email,$role,$phone=null,$organization=null,$occupation=null)
{
$fosUserManager=$this->container->get('fos_user.user_manager');
/**
* #var AppBundle\Entity\User
*/
$user=$fosUserManager->createUser();
$user->setUsername($username);
$user->setEmail($email);
$user->setPlainPassword($password);
$user->setEnabled(true);
$user->setRoles(array($role));
$user->setName($name);
$user->setSurname($surname);
if($phone){
$user->setPhone($phone);
}
if($organization){
$user->setOrganization($organization);
}
if($occupation){
$user->setOccupation($occupation);
}
$fosUserManager->updateUser($user, true);
return $user;
}
/**
* {#inheritDoc}
*/
public function load(ObjectManager $manager)
{
$this->createUser('John','Doe','jdoe','simplepasswd','jdoe#example.com','ROLE_USER','+3021456742324','Acme Products','Soft Engineer');
$this->createUser('Jackie','Chan','jchan','thesimplepasswd','jackiechan#example.com','ROLE_ADMIN','+302141232324','Holywood','Actor');
$this->createUser('Chuck','Norris','chuck_norris','unhackablepasswd','chucknorris#example.com','ROLE_SUPERADMIN',null,'Universe','Master');
}
public function getOrder()
{
return 1;
}
}
But for some reason I get the following error:
There was 1 error:
1) Tests\AppBundle\Controller\DefaultControllerTest::testIndex
Error: Call to a member function get() on null
/home/vagrant/code/src/AppBundle/DataFixtures/Test/DummyUserFixtures.php:50
/home/vagrant/code/src/AppBundle/DataFixtures/Test/DummyUserFixtures.php:87
/home/vagrant/code/tests/AppBundle/Controller/DefaultControllerTest.php:19
Further debugging has proved that the error is triggered by the following line in DummyUserFixtures:
$fosUserManager=$this->container->get('fos_user.user_manager');
So do you know how to load the data via fixtures?
In order to get it working you should set the service container you generate from the static::createClient() method and pass it via the $fixture->setContainer($container)
So a good approach is to define the container as protected instance variable to the BasicHttpController so any Test class (eg. the DefaultControllerTest in your case) is able to load the fixtures accordingly.
So using the setUp method and instance variables of BasicHttpController should be the following:
//Namespace declaration goes there
class BasicHttpController extends WebTestCase
{
protected $entityManager=null;
protected $client=null;
protected $container=null;
/**
* {#inheritdoc}
*/
public function setUp()
{
$this->client = static::createClient();
$this->container = $this->client->getContainer();
$doctrine = $this->container->get('doctrine');
$this->entityManager=$doctrine->getManager();
}
// Rest methods here
}
Note: on classes that are getting inherited from BasicHttpController you can define the setUp like that:
public function setUp()
{
parent::setUp();
// Add extra stuff here
}
So you can do more setUp bootstrapping before tests.
I am currently working on developing a project in SAAS, each client can access his platform by a personalized url (site1.com, site2.com, etc.).
For each domain name a set of template customization data is defined in the back office and I must be able to access it from my Twig files. So I defined a listener on the kernerl.request event that adds a global variable to Twig based on the current domain name. Everything works fine in most cases, except when a page is first displayed, Twig must be run upstream and I get the following error:
Unable to add global "site" as the runtime or the extensions have
already been initialized.
Listener class
class SiteListener
{
public function __construct(
SiteHelper $siteHelper,
\Twig_Environment $twig
) {
$this->siteHelper = $siteHelper;
$this->twig = $twig;
}
/**
* Add current contexts to twig global.
*/
public function addContextsToTwigGlobal(GetResponseEvent $event)
{
$this->twig->addGlobal('site', $this->siteHelper);
}
}
Listener service declaration
multisite.listener.site:
class: MultisiteBundle\Listener\SiteListener
arguments:
- "#multisite.helper.site"
- "#twig"
tags:
- { name: kernel.event_listener, event: kernel.request, method: addContextsToTwigGlobal }
SiteHelper service
class SiteHelper
{
/**
* #var RequestStack
*/
protected $requestStack;
/**
* #var ContextConfigManager;
*/
protected $contextConfigManager;
/**
* #var ContextConfig
*/
protected $contextConfig;
public function __construct(
RequestStack $requestStack,
ContextConfigManager $contextConfigManager
) {
$this->requestStack = $requestStack;
$this->contextConfigManager = $contextConfigManager;
$this->contextConfig = $this->contextConfigManager
->findByHostOrStandard($this->getHost());
}
/**
* Get host from current request.
*
* #return string|null
*/
public function getHost()
{
$request = $this->requestStack->getCurrentRequest();
return ($request) ? $request->getHost() : null;
}
/**
* Get current context config
*
* #return ContextConfig
*/
public function getContextConfig()
{
return $this->contextConfig;
}
}
Any idea ?
I decided to write a Twig function to avoid this kind of problem. This seem to be a good solution.
class SiteExtension extends \Twig_Extension
{
/**
* #var SiteHelper
*/
private $siteHelper;
/**
* Constructor.
*
* #param SiteHelper $siteHelper
*/
public function __construct(SiteHelper $siteHelper)
{
$this->siteHelper = $siteHelper;
}
/**
* {#inheritdoc}
*/
public function getFunctions()
{
return array(
new \Twig_Function('site', array($this->siteHelper, 'getContext')),
);
}
}
I have tried for days to get this to work.
I am using this framework: https://github.com/DennisSkoko/discord-bot to create a bot. But I wan't to add the DIC container http://container.thephpleague.com/ to this project.
What I want to do is in the Main class is to register service providers (modules/packages/bundles) or whatever you want to call them, just like Laravel etc. does.
So I created a module/ dir and added a serviceprovider class and some other classes as the docs state: http://container.thephpleague.com/service-providers/
First I edited the start.php to inject the container:
use League\Container\Container;
require "vendor/autoload.php";
require "bot.php";
$container = new Container;
$main = new DS\Main(new Example($container));
$main->run();
I changed the Example bot to include a getContainer() function so I could read it from the Main class, like this:
use DS\Bot;
use Discord\Discord;
use DS\Service;
class Example extends Bot
{
/**
* Example constructor
*/
public function __construct($container)
{
$this->config = [
"token" => "My token"
];
$this->container = $container;
}
/**
* Will be executed when the WebSocket is ready and before the services are implemented.
*
* #param Discord $discord
*
* #return void
*/
public function setup(Discord $discord)
{
echo "Example is ready to start!";
}
public function getContainer()
{
return $this->container;
}
/**
* Will return an array of Service that the bot uses.
*
* #return Service[]
*/
public function getServices()
{
return [];
}
}
I then changed the Main class to to register my service provider.
namespace DS;
use Discord\Discord;
/**
* A class that bring bots script into life.
*/
class Main
{
/**
* #var Discord
*/
private $discord;
/**
* #var Bot
*/
private $bot;
/**
* Main constructor.
*
* #param Bot $bot
*/
public function __construct(Bot $bot)
{
$this->bot = $bot;
$this->discord = new Discord($this->bot->getConfig());
$this->discord->on("ready", function ($discord) {
$this->bot->setup($discord);
$this->setServices($this->bot->getServices());
});
$this->container = $this->bot->getContainer();
$this->container->addServiceProvider('Mynamespace\HelloWorld\ServiceProvider');
}
/**
* Will run the bot.
*
* #return void
*/
public function run()
{
$this->discord->run();
}
/**
* Will add the services for the WebSocket.
*
* #param Service[] $services
*
* #return void
*/
private function setServices($services)
{
foreach ($services as $service) {
$this->discord->on($service->getEvent(), $service->getListener());
}
}
}
Here is the problem:
PHP Fatal error: Uncaught exception 'InvalidArgumentException' with message 'A service provider must be a fully qualified class name or instance of (\League\Container\ServiceProvider\ServiceProviderInterface)'
Which is weird because I did it just like in the documentation and extended League\Container\ServiceProvider\AbstractServiceProvider
This I have double checked 20 times.
So I have no idea what to do about this. How can I use addServiceProvider() in the Main class and register things in the DIC?
I do not really want to do it in the Example class if possible. Because that should be extended by the user and the Main class will bootstrap the bot.
I also tried $this->bot->addServiceProvider() but then I get the following error:
PHP Fatal error: Call to undefined method Example::addServiceProvider()
Any help appreciated.
Description:
I have a simple class that creates a symlink to a directory of uploaded files that are only available to registered members. It uses the current users session id in order to generate the random directory for the user. Once the user logs out, the symlink is removed. I would like to unit test the functionality of the class.
Question:
How do I go about properly unit testing this class since most functions are private, and I don't see any reason to make them public?
Here is the code for the PHP class:
<?php
namespace Test\BackEnd\MemberBundle\Library;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\KernelInterface;
class DirectoryProtector
{
/** #var SessionInterface $_session */
private $_session;
/** #var ContainerInterface $_kernel */
private $_kernel;
/**
* #param SessionInterface $session
* #param KernelInterface $kernel
*/
public function __construct( SessionInterface $session, KernelInterface $kernel )
{
$this->_session = $session;
$this->_kernel = $kernel;
}
/**
* #param bool|false $protect
* Public method to symlink directories
*/
public function protectDirectory($protect = FALSE)
{
if ($protect) {
if ( ! $this->doesDirectoryExists())
symlink($this->getAppDir() . '/uploads', $this->getViewableSessionDirectory());
} else {
if ($this->doesDirectoryExists())
unlink($this->getViewableSessionDirectory());
}
}
/**
* #return bool
* Check to see if viewable session directory exists or not
*/
private function doesDirectoryExists()
{
if (file_exists($this->getViewableSessionDirectory()))
return TRUE;
return FALSE;
}
/**
* #return string
* Get viewable session full directory path
*/
private function getViewableSessionDirectory()
{
return $this->getAppDir() . '/../web/files/' . $this->getSessionId();
}
/**
* #return string
* Return app root directory
*/
private function getAppDir()
{
return $this->_kernel->getRootDir();
}
/**
* #return string
* Return session id
*/
private function getSessionId()
{
return $this->_session->getId();
}
}
Here is the code for the current test class:
<?php
namespace Test\BackEnd\MemberBundle\Tests\Library;
use Test\BackEnd\MemberBundle\Library\DirectoryProtector;
class DirectoryProtectorTest extends \PHPUnit_Framework_TestCase
{
public function testProtectDirectory()
{
//$this->markTestIncomplete("WIP on protect directory.");
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')
->getMock();
$container = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')
->getMock();
/** #var DirectoryProtector $dp */
$dp = $this->getMockBuilder('Test\BackEnd\MemberBundle\Library\DirectoryProtector')
->setConstructorArgs(array($request, $container))
->setMethods(array(
'getViewableSessionDirectory',
'getAppDir',
'getSessionId'
))
->getMock();
$dp->expects($this->once())
->method('doesDirectoryExists')
->will($this->returnValue(TRUE));
$dp->protectDirectory(TRUE);
}
}
From https://phpunit.de/manual/current/en/test-doubles.html
Limitation: final, private, and static methods
Please note that final, private and static methods cannot be stubbed or mocked. They are ignored by PHPUnit's test double functionality and retain their original behavior.
Is not a good practice to unit test private or protected methods. You should test the public API. Private methods are supposed to be tested indirectly through the API. That said, you can make the method public with reflection:
$instance = new DirectoryProtector(...);
$ref = new \ReflectionClass('DirectoryProtector');
$method = $ref->getMethod('doesDirectoryExists');
$method->setAccessible(true);
$this->assertTrue($method->invoke($instance));
I think your approach is incorrect. You shouldn't mock method, but objects that are passed by injection in constructor.
For the SessionInterface you can pass MockArraySessionStorage. For ContainerInterface I don't know which class you are using, so I add some demo example. Test case:
public function testProtectDirectory()
{
$session = new MockArraySessionStorage();
$session->setId('123123');
$kernel = \Ouzo\Tests\Mock\Mock::create('ContainerInterface');
\Ouzo\Tests\Mock\Mock::when($kernel)->getRootDir()->thenReturn('/root_dir');
$directoryProtector = new DirectoryProtector($session, $kernel);
$directoryProtector->protectDirectory(true);
//asserts
}
and your methods will be returning:
getViewableSessionDirectory -> /root_dir/../web/files/123123
getAppDir -> /root_dir
And remember changing methods accessible is always bad idea.
PS. For mocking I use utils form Ouzo framework. Of course you can use other mocking framework.