Deal with 'new' keyword in PHP unit test - php

In my case I have a class such as:
class Logger
{
/**
* #var EntityManager
*/
protected $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #param Model $model
*/
public function log(Model $model)
{
$logEntity = new LogEntity();
$logEntity->setOrder($model->getOrder());
$logEntity->setType($model->getType());
$logEntity->setScore($model->getScore());
$logEntity->setCreated(new DateTime());
$logEntity->setModified(new DateTime());
$this->entityManager->persist($logEntity);
$this->entityManager->flush();
return $logEntity;
}
Logger class is not testable because in my code 'new' keyword exists, in other hand Logger class and EntityManager class registered singleton in container and can't inject model as dependency.
How to change class for change to testable class?
LoggerModel is a Doctrine entity and use in Laravel framework.

I solve this problem with a sample solution: Factory Pattern.
I need assertion, so when get a new model from factory assert it as mock.
And how?
I create a class with a method that can be a singleton service:
class LogFactory
{
public function makeLogEntity()
{
return new LogEntity();
}
}
In another service, inject factory class:
class Logger
{
/**
* #var EntityManager
*/
protected $entityManager;
/**
* #var LogFactory
*/
protected $logFactory;
public function __construct(EntityManager $entityManager, LogFactory $logFactory)
{
$this->entityManager = $entityManager;
$this->logFactory = $logFactory
}
/**
* #param Model $model
*/
public function log(Model $model)
{
$logEntity = $this->logFactory->makeLogEntity();
$logEntity->setOrder($model->getOrder());
$logEntity->setType($model->getType());
$logEntity->setScore($model->getScore());
$logEntity->setCreated(new DateTime());
$logEntity->setModified(new DateTime());
$this->entityManager->persist($logEntity);
$this->entityManager->flush();
return $logEntity;
}
Now I have a service that is mock able and call $mock->willReturn() function in test.

Related

Test Service with phpunit inside symfony


How can I test Services with PHPUnit using symfony? So far, I installed and included test-pack, DAMA Doctrine Bundle, and created Test Database.
Inside .env.test I added Database connection
# define your env variables for the test env here
KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
# .env.test.local
DATABASE_URL="mysql://root:root#db:3306/testdb?serverVersion=mariadb-10.4.11&charset=utf8mb4"
I included inside phpunit.xml.dist the DAMA Doctrine bundle
<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>
Now, what I want to test is my Services (for instance CartService, ProductService etc.)
use App\Entity\Cart;
use App\Entity\CartItem;
use App\Entity\Product;
use App\Entity\User;
use App\Repository\CartItemRepository;
use App\Repository\CartRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Security;
class CartService
{
private CartRepository $cartRepository;
private ManagerRegistry $managerRegistry;
private CartItemRepository $cartItemRepository;
private Security $security;
public function __construct(Security $security, CartItemRepository $cartItemRepository, CartRepository $cartRepository, ManagerRegistry $managerRegistry)
{
$this->cartItemRepository = $cartItemRepository;
$this->cartRepository = $cartRepository;
$this->managerRegistry = $managerRegistry;
$this->security = $security;
}
/**
* Get Cart by ID
*
* #return Cart|null
*/
public function getCartByUserId(): ?Cart
{
$user = $this->security->getUser();
return $this->cartRepository->findOneBy(['customer' => $user]);
}
/**
* Show Cart and Total Price
*
* #return Cart|null
*/
public function showCart(): ?Cart
{
$cart = $this->getCartByUserId();
$this->calculateTotalPrice();
return $cart;
}
When I run phpunit test on CartServiceTest, I get this error:
1) App\Tests\CartServiceTest::testShowCart
Error: Typed property App\Tests\CartServiceTest::$cartService must not be accessed before initialization
/var/www/html/Tests/CartServiceTest.php:29
CartServiceTest look like this
<?php
namespace App\Tests;
use App\Entity\Product;
use App\Service\CartService;
use Doctrine\ORM\EntityManager;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class CartServiceTest extends KernelTestCase
{
/**
* #var EntityManager
*/
private EntityManager $entityManager;
private CartService $cartService;
public function setUp(): void
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
}
public function testShowCart()
{
$user = 11;
$cart = $this->cartService->getCartByUserId();
dump($cart);
}
protected function tearDown(): void
{
$this->entityManager->close();
}
}
Error: Typed property App\Tests\CartServiceTest::$cartService must not be accessed before initialization
Means that you have to already Use the cartService in your application. For exemple if you already inject this service has a dependency injection in one of your controller it's okay.
But you can do better. Just create a service config for your tests "services_test.yaml" and make your service public
Something like:
#servies_test.yaml
services:
App\Service\CartService:
public: true

How to autowire an entity into a service?

I have an abstract class called AbstractMediaService and a some specific implementations of this abstract class:
abstract class AbstractMediaService
{
private $em;
private $media;
public function __construct(EntityManagerInterface $em, Media $media)
{
$this->em = $em;
$this->media = $media;
}
public function dosomethingInCommon();
abstract public function dosomethingSpecific();
}
class PhotoMediaService extends AbstractMediaService
{
public function dosomethingSpecific()
{
echo 'i am a photo service';
}
}
class VideoMediaService extends AbstractMedia
{
public function dosomethingSpecific()
{
echo 'i am a video service';
}
}
These objects require a Media entity to work with
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
class Media
{}
Controller
/**
* #Route("/{_locale}/infos/{idMedia}.html", name="info", methods={"GET"}, requirements={
* "idMedia" = "\d+",
* })
*/
public function infosPhotoAction(RequestStack $requestStack, Media $media)
{
$request = $requestStack->getCurrentRequest();
$session = $requestStack->getSession();
$media = new PhotoMedia($media);
// return response
}
Problem is that I need some dependencies like the Security service or the EntityManager.
I would like to know how autowire AbstractMediaService service.
This is wrong. You cannot autowire Media to be injected into a service, because entities are not services.
public function __construct(EntityManagerInterface $em, Media $media)
If VideoMediaService and PhotoMediaService (I renamed them for clarity, since sharing the name with your entity made it look like it were related) need an instance of Media to perform some work, just make that a parameter for the corresponding methods.
public function dosomethingInCommon(Media $media);
abstract public function dosomethingSpecific(Media $media);
Or alternatively, simply have a setMedia(Media $media) method on that class for that:
public function setMedia(Media $media) {
$this->media = $media;
}
Frankly, this latter approach does not seem like a great idea. You would need to make the methods that work on $media aware of the possibility of setMedia() not having been called yet, or subsequent calls to setMedia() would change how the service behaved. Just making it a parameter of the appropriate method is much cleaner, clearer and safer.
Injecting those services is done like any other service. That they extend an abstract class is irrelevant.
/**
* #Route("/{_locale}/infos/{idMedia}.html", name="info", methods= {"GET"}, requirements={
* "idMedia" = "\d+",
* })
*/
public function infosPhotoAction(RequestStack $requestStack, Media $media, PhotoMediaService $photoMediaService): Response
{
$request = $requestStack->getCurrentRequest();
$session = $requestStack->getSession();
$photoMediaService->doSomethingSpecific($media)
return new Response('all done');
}

Symfony 5.1 - Cannot autowire service

I want to refactoring my code, but now i have error and don't understand what.
Objectif : Don't need to pass parameters when call TokenService, and use autowiring to autowiring EntityManager & Request, and don't set it when controller call service.
Cannot resolve argument $tokenService of App\Controller\TokenController::showTokens()
Cannot autowire service App\Service\TokenService
argument $request of method __construct() references class Symfony\Component\HttpFoundation\Request but no such service exists.
Before :
/src/Controller/TokenController.php
<?php
namespace App\Controller;
use App\Service\TokenService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* #Route("/v1")
*/
class TokenController
{
/** #var EntityManagerInterface $em */
private $em;
/** #var Request $request */
private $request;
/**
* TokenService constructor.
*
* #param Request $request
* #param EntityManagerInterface $em
*/
public function __construct(Request $request, EntityManagerInterface $em)
{
$this->request = $request;
$this->em = $em;
}
public function showTokens(Request $request, EntityManagerInterface $em): JsonResponse
{
$tokenService = new TokenService($request, $em);
return $tokenService->getTokens();
}
}
/src/Service/TokenService.php
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Class TokenService
* #package App\Service
*/
class TokenService
{
/** #var EntityManagerInterface $em */
private $em;
/** #var Request $request */
private $request;
/**
* TokenService constructor.
*
* #param Request $request
* #param EntityManagerInterface $em
*/
public function __construct(Request $request, EntityManagerInterface $em)
{
$this->request = $request;
$this->em = $em;
}
public function getTokens()
{
return true;
}
}
After :
/config/services.yaml
parameters:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
App\Service\TokenService: ~
/src/Controller/TokenController.php
<?php
namespace App\Controller;
use App\Service\TokenService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* #Route("/v1")
*/
class TokenController
{
public function showTokens(Request $request, EntityManagerInterface $em, TokenService $tokenService): JsonResponse
{
return $tokenService->getTokens();
}
/src/Service/TokenService.php
<?php
namespace App\Service;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Class TokenService
* #package App\Service
*/
class TokenService
{
/** #var EntityManagerInterface $em */
private $em;
/** #var Request $request */
private $request;
/**
* TokenService constructor.
*
* #param Request $request
* #param EntityManagerInterface $em
*/
public function __construct(Request $request, EntityManagerInterface $em)
{
$this->request = $request;
$this->em = $em;
}
public function getTokens()
{
return true;
}
}
Thanks !
I guess it's been awhile since we had a good request stack question. I did a bit of a search and did not find any answer that was directly applicable and provided a decent explanation.
The basic issue is that the Symfony framework supports nested requests. You get these, for example, when using embedded controllers. So there is no actual request service. There actually used to be when Symfony 2.0 was first released but it was a real mess. Supporting nested request services was done at the container level and it was not fun.
So a big hammer known as the request stack was introduced to solve the problem once and for all. You inject the request stack instead of the request and then access the request when you actually need it.
class TokenService
{
private $em;
private $requestStack;
public function __construct(RequestStack $requestStack, EntityManagerInterface $em)
{
$this->requestStack = $requestStack;
$this->em = $em;
}
public function getTokens()
{
$request = $this->requestStack->getMasterRequest(); // or possibly getCurrentRequest depending on where the tokens are
return true;
}
Having said that, I would personally just pass the request from the controller. Doing so gets rid of that 'it depends' comment of mine. I also thinks it reduces the 'magic' involved just a bit. Both approaches will work.
class TokenService
{
public function getTokens(Request $request)
{
return true;
}
...
class TokenController
{
public function showTokens(Request $request, TokenService $tokenService): JsonResponse
{
return $tokenService->getTokens($request);
}

Symfony 3 Inject service into entity repository

I tried inject memcached service into entity repository, but my variant not work.
services:
work.repository.company:
class: WorkBundle\Repository\CompanyRepository
factory: ['#doctrine.orm.entity_manager', getRepository]
arguments:
- 'WorkBundle:Company'
calls:
- [setCacheService, ['#memcache.default']]
CompanyRepository have setter setCacheService, but it's not called.
class CompanyExtension extends \Twig_Extension
{
/**
* #var EntityManager
*/
private $em;
public function setEntityManager(EntityManager $entityManager)
{
$this->em = $entityManager;
}
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('getCompaniesCount', array($this, 'getCompaniesCount'))
);
}
/**
* #return integer
*/
public function getCompaniesCount()
{
return $this->em->getRepository('WorkBundle:Company')->getActiveCompaniesCount();
}
public function getName()
{
return 'work_company_extension';
}
}
Why this code not works?
Have you registered repositoryClass in your WorkBundle:Company entity? Your entity should contain something like: #ORM\Entity(repositoryClass="Work\Company") or yaml equivalent.
You should let Symfony create the repository by injecting work.repository.company into your Twig extension.

Must I move data dependency out of my Controllers (and into Factories)?

This question can be viewed through a prism of ZF2 + Doctrine + MVC programming practices, or it can be viewed through just an OOP perspective.
My concern is about Separation of Concerns, and on removing dependencies.
I am using code in my controllers that goes something like this:
class MyController
{
private $em; //entityManager
function __construct()
{
$this->em = DoctrineConnector::getEntityManager();
}
function indexAction()
{
//Input
$inputParameter = filter_input(...);
//request for Data
$queryBuilder = $this->em->createQuery(...)
->setParameter('param', $inputParameter);
$query = $queryBuilder->getQuery();
//$services is the user-defined data type requested
$services = $query->getResult();
//use data to produce a view model
$view = new ViewModel();
$view->setVariables(array('services' => $services));
return $view;
}
}
I am not entirely comfortable with the above and wanted a second opinion. For one, my EntityManager is part of the class, so my class is cognizant of the entity manager construct, when I think it should not be a part of the controller. Do I perhaps use a Factory or Builder design pattern to help me create MyController class?
If I do, I can move my em (entityManager) construct into the Factory pattern and create and populate my MyController inside the Factory. Then, the MyController can have a private variable $services instead.
i.e.
class MyController
{
private $services;
function setServices($services)
{
$this->services = $services;
}
function indexAction()
{
//use data to produce a view model
$view = new ViewModel();
$view->setVariables(array('services' => $this->services));
return $view;
}
}
class MyFactoryMethod
{
function createMyController()
{
//Input
$inputParameter = filter_input(INPUT_GET...);
//request for Data
$queryBuilder = $this->em->createQuery(...)
->setParameter('param', $inputParameter);
$query = $queryBuilder->getQuery();
//$services is the user-defined data type requested
$services = $query->getResult();
//create and return MyController instance
$controller = new MyController();
$controller->setServices($services);
return $controller;
}
}
I typically tried to do this PHP's mysql extension to remove dependency on data out of my various objects. I am using Doctrine2 now which is an ORM, and wondering if I should keep doing the same thing (namely preferring 2nd example rather than the first...
Question:
I can write code both ways. It works essentially the same. My question is -- is the code, as it is written in my 2nd example preferred more than the code as it is written in my first?
Notes / Clarifications:
In my case variable $services is a domain-specific variable (not ZF2's ServiceLocator). i.e. think of MyController as a controller for business-specific "services".
I am not harnessing full power of ZF2 with configs, routers, events, and everything. I am using ZF2 modules on an existing legacy codebase on as-needed basis.
When your controller has hard dependencies I would suggest to use the common ZF2 solution by creating the controller and injecting the dependency in a factory instance and registering the controller under the 'factories' key in your 'controllers' config array.
In your module.config.php
'controllers' => array(
'factories' => array(
'Application\Controller\MyController' => 'Application\Controller\MyControllerFactory'
)
)
In your controller I would set hard dependency in the __construct method. Like this you prevent the controller from ever being instantiated without your dependencies (it will throw an exception).
Never inject something like $services (if this is a ServiceLocator) from which you will pull the actual dependencies since it is not clear what the class actually needs. It will be harder to understand for other developers and it is also hard to test since you cannot set mocks for your individual dependencies so easily.
Your Controller class:
<?php
namespace Application\Controller;
use Doctrine\ORM\EntityManager;
class MyController
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
*
*/
function indexAction()
{
//Do stuff
$entityManager = $this->getEntityManager();
}
/**
* #return EntityManager
*/
public function getEntityManager()
{
return $this->entityManager;
}
}
Your Factory:
<?php
namespace Application\Controller;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Doctrine\ORM\EntityManager;
class MyControllerFactory implements FactoryInterface
{
/**
* #param ServiceLocatorInterface $serviceLocator
* #return MyController
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
/** #var EntityManager $entityManager */
$serviceManager = $serviceLocator->getServiceLocator()
$entityManager = $serviceManager->get('doctrine.entitymanager.orm_default');
$myController = new MyController($entityManager);
return $myController;
}
}
There are two different approaches to this problem that are provided by ZF2.
Use the ServiceLocator to retrieve the EntityManager via a Factory.
In Module.php, add an anonymous function or Factory.
public function getServiceConfig()
{
return [
'factories' => [
'Doctrine\ORM\EntityManager' => function (ServiceManager $sm) {
$entityManager = $sm->get('doctrine.entitymanager.orm_default');
return $entityManager;
}
],
],
}
In your Controller
$em = $this->getServiceLocator()->get('Doctrine\ORM\EntityManager');
Create an Initializer and AwareInterface to inject the EntityManger into your controllers.
The AwareInterface can be added to any class which is initialized by the ServiceManager.
interface EntityManagerAwareInterface
{
/**
* Set EntityManager locator
*
* #param EntityManager $entityManager
*/
public function setEntityManager(EntityManager $entityManager);
/**
* Get service locator
*
* #return EntityManager
*/
public function getServiceLocator();
}
The Initializer is run when services are initialized by the ServiceManager. A check is performed to so if $instance is a EntityManagerAwareInterface.
use Application\EntityManager\EntityManagerAwareInterface;
use Zend\ServiceManager\InitializerInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class EntityManagerInitializer implements InitializerInterface
{
/**
* Initialize
*
* #param $instance
* #param ServiceLocatorInterface $serviceLocator
* #return mixed
*/
public function initialize($instance, ServiceLocatorInterface $serviceLocator)
{
if ($instance instanceof EntityManagerAwareInterface) {
$entityManager = $serviceLocator->get('doctrine.entitymanager.orm_default');
$instance->setEntityManager($entityManager);
}
}
}
Next add the Initializer to Module.php
public function getServiceConfig()
{
return [
'initializers' => [
'entityManager' => new EntityManagerInitializer(),
],
],
}
The advantage of going the Initializer route is there is a one time setup. Any class that implements the EntityManagerAwareInterface will have the EntityManager injected when the class is initialized.

Categories