Was testing an application and was repeatable getting the infamous new entity not configured to cascade persist error. I was surprised since I wasn't even creating new entities, and after digging into it, it appears to be relate to using different instances of the EntityManager object (I have confirmed that they are working with the same database, however) which I guess makes sense since each test will have a transaction applied. The only way I was able to get rid of the errors was to use the entityManager in the container instead of the autowired ones. While it works, it is a bit of a kludge and I would like to know the right way of doing this. Thank you
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
class MyTest extends ApiTestCase
{
/**
* #dataProvider getData
*/
public function testWhichDoesNotWork(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$user = $service->getUser();
$randomEntity = $service->getRandomEntity($user->getTenant(), $class);
$randomEntity->setSomething('something');
$service->saveEntity($randomEntity);
}
/**
* #dataProvider getData
*/
public function testWhichWorks(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$em = static::getContainer()->get(EntityManagerInterface::class);
$user = $service->getUser();
$randomId = $service->getRandomEntityId($user->getTenant(), $class);
$randomEntity = $em->getRepository($class)->find($randomId);
$randomEntity->setSomething('something');
$em->persist($randomEntity);
$em->flush();
}
/**
* #dataProvider getData
*/
public function testAnotherWhichWorks(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$service->setNewEntityManager(static::getContainer()->get(EntityManagerInterface::class));
$user = $service->getUser();
$randomEntity = $service->getRandomEntity($user->getTenant(), $class);
$randomEntity->setSomething('something');
$service->saveEntity($randomEntity);
}
public function getData(): array
{
return [
[123, SomeClass::class]
];
}
}
namespace App\Test\Service;
final class MyService
{
public function __construct(private EntityManagerInterface $entityManager)
{}
public function setNewEntityManager(EntityManagerInterface $entityManager):self
{
$this->entityManager = $entityManager;
return $this;
}
public function getDatabase():string
{
return $this->entityManager->getConnection()->getDatabase();
}
public function getUser(int $id):User
{
return $this->entityManager->getRepository(User::class)->find($id);
}
public function getRandomId(Tenant $tenant, string $class):int
{
$meta = $this->entityManager->getClassMetadata($class);
$_sql = 'SELECT %s FROM public.%s WHERE tenant_id=? OFFSET floor(random() * (SELECT COUNT(*) FROM public.%s WHERE tenant_id=?)) LIMIT 1;';
$sql = sprintf($_sql, $meta->getSingleIdentifierFieldName(), $meta->getTableName(), $meta->getTableName());
return $this->entityManager->getConnection()->prepare($sql)->execute([$tenant->getId(), $tenant->getId()])->fetchOne();
}
public function getRandomEntity(Tenant $tenant, string $class):object
{
return $this->entityManager->getRepository($class)->find($this->getRandomId($tenant, $class));
}
public function saveEntity(object $entity):self
{
$this->entityManager->persist($entity);
$this->flush();
return $this;
}
}
services:
app.test.my.service:
alias: App\Test\Service\MyService
public: true
Related
I am running phpunit version 9.2 and I would like to know why my method is not covered in the phpunit coverage.
This is my class:
class Methods extends Template
{
const DISABLED_PAYMENT_METHODS_1 = 'free';
const DISABLED_PAYMENT_METHODS_2 = 'adyen_cc';
const DISABLED_PAYMENT_METHODS_3 = 'adyen_oneclick';
protected $paymentMethodList;
protected $storeManager;
protected $logger;
public function __construct(
Context $context,
PaymentMethodList $paymentMethodList,
StoreManagerInterface $storeManager,
LoggerInterface $logger,
array $data = []
) {
$this->paymentMethodList = $paymentMethodList;
$this->storeManager = $storeManager;
$this->logger = $logger;
parent::__construct($context, $data);
}
public function getPaymentMethods()
{
try {
$storeId = $this->storeManager->getStore()->getId();
$paymentList = $this->paymentMethodList->getActiveList($storeId);
$resultPayments = [];
foreach ($paymentList as $payment) {
if ($payment->getCode() !== self::DISABLED_PAYMENT_METHODS_1 &&
$payment->getCode() !== self::DISABLED_PAYMENT_METHODS_2 &&
$payment->getCode() !== self::DISABLED_PAYMENT_METHODS_3
) {
$resultPayments[] = $payment;
}
}
return $resultPayments;
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
}
}
and this is my test class:
class MethodsTest extends TestCase
{
private $model;
private function getSimpleMock($originalClassName)
{
return $this->getMockBuilder($originalClassName)
->disableOriginalConstructor()
->getMock();
}
public function setUp() : void
{
$context = $this->getSimpleMock(Context::class);
$paymentMethodList = $this->getSimpleMock(PaymentMethodList::class);
$storeManager = $this->getSimpleMock(StoreManagerInterface::class);
$logger = $this->getSimpleMock(LoggerInterface::class);
$this->model = new Methods(
$context,
$paymentMethodList,
$storeManager,
$logger,
[]
);
}
public function testGetPaymentMethods()
{
$stub = $this->createMock(Methods::class);
$stub->method('getPaymentMethods')
->willReturn([]);
try {
$stub->getPaymentMethods();
$this->fail("Expected exception!");
} catch (\Exception $error) {
$this->assertEquals("Expected exception!", $error->getMessage());
}
}
}
When I run the command to get the coverage. I am getting:
I am really curious why my test is not covered or at least the exception part ? Would you please share you ideas why ? and what can i do in order to fix this ? Right now I got a 29 % and I would like to get at least 60% coverage.
Thank you
On this line $stub = $this->createMock(Methods::class); you are creating a mock of the Methods class, so not actually testing the real class.
You will need to use the object you created in your setUp() method, and set up mock returns on the dependencies you passed in (perhaps converting some of them to be class properties).
You should test the real class, as example:
public function testGetPaymentMethods()
{
// define a payment
$paymentFreeCode = $this->createMock(Payment::class);
$paymentFreeCode->method('getcode')
->willReturn("free");
// define a payment
$payment = $this->createMock(Payment::class);
$payment->method('getcode')
->willReturn("invalid-code");
$paymentList = [
$paymentFreeCode,
$payment,
];
// define a store
$store = $this->createMock(Store::class);
$store->method('getId')
->willReturn("my-store-id");
// return store from the store manager
$this->storeManager->method('getStore')
->willReturn(myStore);
// return the payment list
$this->paymentMethodList->method('getActiveList')->with("my-store-id")
->willReturn($paymentList);
// call the real class instrumented with mocks
$paymentMethods = $this->model->getPaymentMethods();
$this->assertIsArray($paymentMethods);
$this->assertCount($paymentMethods, 1);
}
I used to have a PHPUnit code like that (probably not a good one as I am not able to rewrite it):
$authorizator->expects($this->at(0))
->method('isAllowed')
->willReturn($hasSuperPrivilege);
if (!$hasSuperPrivilege) {
$authorizator->expects($this->at(1))
->method('isAllowed')
->willReturn($hasStandardPrivilege);
}
How to achieve the same behavior after the deprecation of at() method?
I don't know how many times the method will be called, so probably the willReturnOnConsecutiveCalls way is not the right one?
If the first call returns false, then the isAllowed() method must be called twice, if it returned true, then only once.
Related issue: https://github.com/sebastianbergmann/phpunit/issues/4297
Tested method:
public function canUserdoSomething(Foo $resource, Identity $identity): bool
{
if (
$this->authorizator->isAllowed(
$identity,
$resource,
'superPrivilege'
)
) {
return true;
}
if (
$this->authorizator->isAllowed(
$identity,
$resource,
'standardPrivilege'
)
) {
return $resource->hasSomeProperty();
}
return false;
}
I'd go with a handwritten fake so you don't have to worry about the number of calls. I did it inline in my example for brevity but if you're using the authorizator in multiple test, it makes sense to extract it into a separate class.
/**
* #covers \MyClass
*/
class MyClassTest extends TestCase
{
private $subject;
private $authorizator;
/**
* #testWith ["superPrivilege", false, true]
* ["standardPrivilege", true, true]
* ["standardPrivilege", false, false]
* ["invalidPrivilege", true, false]
*/
public function testCanUserdoSomething($privelege, $resourceProperty, $expected)
{
$identity = new Identity();
$resource = new Foo($resourceProperty);
$this->authorizator->addPrivilege($identity, $resource, $privelege);
$result = $this->subject->canUserdoSomething($resource, $identity);
self::assertEquals($expected, $result);
}
protected function setUp(): void
{
parent::setUp();
$this->authorizator = new class implements Authorizator {
private $privileges = [];
public function addPrivilege(Identity $identity, Foo $resource, string $privilege): void
{
$this->privileges[] = spl_object_id($identity) . spl_object_id($resource) . $privilege;
}
public function isAllowed(Identity $identity, Foo $resource, string $privilege): bool
{
return in_array(spl_object_id($identity) . spl_object_id($resource) . $privilege, $this->privileges);
}
};
$this->subject = new MyClass($this->authorizator);
}
}
I'm trying to create a class function which resembles how we used to fetch database listing and convert into a dropdown listing.
eg: DB::table()->where()->get()
what i would like to achieve in laravel custom class or through model is this
Dropdown::fetch()->toArray()
Dropdown::fetch()->toDropdown()
I tried to figure out how this can be done through google. But couldn't find any solution to it.
I'm using laravel 5.8
--
Edit - Sample Code added
Code tried:
namespace App\Http\Models;
use DB;
use Closure;
use BadMethodCallException;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Database\Eloquent\Model;
class Dropdown extends Model
{
private $result = [];
private $default;
public function _cities(){
$tbl_cities = config("tables.TBL_meta_cities");
$result = DB::table($tbl_cities)->select('id', 'cityname')
->orderBy('id')->get()->toArray();
$this->result = $result;
}
public function _select(){
}
public function _list(){
return $this->result;
}
public function _setDefault($def=''){
}
public static function __callStatic($method, $parameters)
{
$action = '_'.$method;
if(method_exists(get_called_class(), $action))
self::$action(...$parameters);
else echo 'not found';
}
public function __call($method, $parameters)
{
$action = '_'.$method;
if(method_exists($get_called_class(), $action))
self::$action(...$parameters);
else echo 'not found';
}
}
and i tried
Dropdown::cities()->list()
but ended with bugs
Well i figured it out myself.
class Dropdown extends Model
{
private static $result = [];
private function getCities(){
$result = City::select('id', 'cityname')
->orderBy('id')->get()->toArray();
self::$result = $result;
}
public function toArray(){
return self::$result;
}
public function toDropdown(){
// Do the dropdown works
}
/**
* Dynamically handle calls to the class.
*
* #param string $method
* #param array $parameters
* #return mixed
*
* #throws \BadMethodCallException
*/
public function __callMethod($method, $parameters){
// Check with inclusive
$class = get_called_class();
$avail = false;
$action = '';
// Check method availability - direct
if(!$avail){
$action = $method;
$avail = method_exists($class, $action);
}
// Check method 2
if(!$avail){
$action = 'get'.ucwords($method);
$avail = method_exists($class, $action);
}
if($avail){
// Call the method
$return = self::$action(...$parameters);
if(!empty($return)) return $return;
} else {
// Throw error if method not found
throw new BadMethodCallException("No such method exists: $name");
}
return new self;
}
public static function __callStatic($method, $parameters){
return (new self)->__callMethod($method, $parameters);
}
public function __call($method, $parameters){
return (new self)->__callMethod($method, $parameters);
}
}
All i need to do is return new self which does the trick instead of return $this so that the trailing function can be called easily.
Now i can able to call that function like this
Dropdown::cities()->toArray();
Reference:
https://stackoverflow.com/a/41631711/1156493
Thank you #Joseph for your time & support.
Using Symfony 4.4 with autowiring activated, I want to instantiate a class using the design-pattern FactoryMethod.
The class instantiated is a service with autowired arguments passed into the constructor.
It work well if the constructor is the same for each type of class to instantiate inside the factory method.
But, each service to instantiate has to autowire some specific service in order to work.
I found that we could use the "setter dependency injection". Articles describing it:
https://symfonycasts.com/screencast/symfony-fundamentals/logger-trait
https://symfony.com/doc/4.4/service_container/injection_types.html#setter-injection
I tried to implement the setter dependency injection but the code inside is never executed.
Considering the articles, we should enter the setters with the PHPDoc "#required" immediately after the __construct method has been called (from what I understood).
It doesn't work with my code (see below).
Is my implementation correct?
Is there a better way of doing it?
My code looks like:
// Controller
/**
*#Route("/my_action/{param}")
*/
public function my_action (ThingManagerFactory $thingManagerFactory, $param)
{
$thingManager = $thingManagerFactory->get($param);
$thingManager->doSomething();
}
// ThingManagerFactory
class ThingManagerFactory
{
private $firstManager;
private $secondManager;
private $thirdManager;
public function __construct(FirstManager $firstManager, SecondManager $secondManager, ThirdManager $thirdManager)
{
$this->firstManager = $firstManager;
$this->secondManager = $secondManager;
$this->thirdManager = $thirdManager;
}
public function get($param): ThingManagerInterface
{
if($param == 1) {
return new Thing1Manager(
$this->firstManager,
$this->secondManager,
$this->thirdManager,
);
} elseif($param == 2) {
return new Thing2Manager(
$this->firstManager,
$this->secondManager,
$this->thirdManager,
);
}
throw new \InvalidArgumentException("...");
}
}
// ThingManagerInterface
interface ThingManagerInterface
{
public function __construct(
$this->firstManager,
$this->secondManager,
$this->thirdManager,
);
public function doSomething();
}
// Thing1Manager
class Thing1Manager implements ThingManagerInterface
{
(...)
private $spec1Manager;
public function __construct(
$this->firstManager,
$this->secondManager,
$this->thirdManager,
)
{
(...)
}
/**
* #required
*/
public function setSpecificManager(Spec1Manager $spec1Manager)
{
// this code is never called
$this->spec1Manager = $spec1Manager;
}
public function doSomething()
{
// we pass here before going into setSpecificManager
(...)
}
}
// Thing2Manager class
// is similar to Thing1Manager with multiple other specific managers.
Thank you for your help.
In order to use the design-pattern Factory Method with Symfony, use the Service Locator to provide autowire outside a Controller.
Refactor the code to the following:
// Controller
/**
*#Route("/my_action/{param}")
*/
public function my_action (ThingManagerFactory $thingManagerFactory, $param)
{
$thingManager = $thingManagerFactory->get($param);
$thingManager->doSomething();
}
// ThingManagerFactory
use App\Locator\ThingLocator;
class ThingManagerFactory
{
private $locator;
public function __construct(ThingLocator $locator)
{
$this->locator = $locator;
}
public function get($param): ThingManagerInterface
{
if($param == 1) {
return $this->locator->get(Thing1Manager::class);
} elseif($param == 2) {
return $this->locator->get(Thing2Manager::class);
}
throw new \InvalidArgumentException("...");
}
}
// ServiceLocatorInterface
interface ServiceLocatorInterface
{
public function get(string $id);
}
// ThingLocator
class ThingLocator implements ServiceLocatorInterface, ServiceSubscriberInterface
{
private $locator;
public function __ construct(ContainerInterface $locator)
{
$this->locator = $locator;
}
public function get(string $id)
{
if (!$this->locator->has($id)) {
throw new \Exception("The entry for the given '$id' identifier was not found.");
}
try {
return $this->locator->get($id);
} catch (ContainerExceptionInterface $e) {
throw new \Exception("Failed to fetch the entry for the given '$id' identifier.");
}
}
public static function getSubscribedServices()
{
return [
Thing1Manager::class,
Thing2Manager::class,
];
}
}
// ThingManagerInterface
interface ThingManagerInterface
{
public function doSomething();
}
// Thing1Manager
class Thing1Manager implements ThingManagerInterface
{
// ...
private $spec1Manager;
public function __construct($firstManager, $secondManager, $thirdManager, $spec1Manager)
{
// ...
}
// This setter is no more needed. This manager can be added to the constructor method.
// **
// * #required
// */
//public function setSpecificManager(Spec1Manager $spec1Manager)
//{
// if not commented, this code would be called thanks to the Service Locator (which is a Symfony Service Container)
// $this->spec1Manager = $spec1Manager;
//}
public function doSomething()
{
// ...
}
}
Consider the next test:
class User
{
}
class UserRepository
{
public function getByName($name)
{
}
public function getByUser(User $user)
{
}
}
class UserController
{
private $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function findByNames($name1, $name2)
{
$this->userRepository->getByName($name1);
$this->userRepository->getByName($name2);
}
public function findByUsers($user1, $user2)
{
$this->userRepository->getByUser($user1);
$this->userRepository->getByUser($user2);
}
}
class WithConsecutiveTest extends \PHPUnit_Framework_TestCase
{
/**
* This test is fails if some of "Alice" or "Bob" string changed. This is expected behaviour.
*/
public function testWithConsecutiveOnStrings()
{
$name1 = 'Alice';
$name2 = 'Bob';
$userRepository = $this->createMock(UserRepository::class);
$userRepository
->expects($this->exactly(2))
->method('getByName')
->withConsecutive(
[$name1], // change to $name2 and test fails
[$name2]
);
$userController = new UserController($userRepository);
$userController->findByNames($name1, $name2);
}
/**
* This test is NOT fails if in "withConsecutive()" call $user1 changed to $user2. This is unexpected behaviour.
*/
public function testWithConsecutiveOnObjects()
{
$user1 = $this->createMock(User::class);
$user2 = $this->createMock(User::class);
$this->assertEquals($user1, $user2);
$this->assertNotSame($user1, $user2);
$userRepository = $this->createMock(UserRepository::class);
$userRepository
->expects($this->exactly(2))
->method('getByUser')
->withConsecutive(
[$user1], // change to $user2 and test is also passed
[$user2]
);
$userController = new UserController($userRepository);
$userController->findByUsers($user1, $user2);
}
}
First test with string arguments for "withConsecutive()" is works correctly, but second test with objects do some magic: something similar to weak comparison of two objects is here, so second test is passed in any case.
I tried with "[$this->callback(function($user1arg) use ($user1)){return $user1arg === $user1}]" constraint - this works well, but write a lot of this constraint is a some kind of monkey job.
Maybe it may have a more simple solution, instead of writing a lot of constraints with $this->callback(...) for objects?
Try this code:
$userRepository
->expects($this->exactly(2))
->method('getByUser')
->withConsecutive(
[$this->identicalTo($user1)],
[$this->identicalTo($user2)]
);
Not everything in PHPUnit is perfect, but this should work for You. Of course property 'name' is only an example. If user does not holds any behaviour, You don't need to mock it.
class User
{
private $name;
public function setName($name)
{
$this->name = $name;
}
public function getName($name)
{
return $this->name;
}
}
test method
public function testWithConsecutiveOnObjects()
{
$user1 = new User();
$user1->setName('Alice');
$user2 = new User();
$user2->setName('Bob');
$this->assertNotEquals($user1, $user2);
$this->assertNotSame($user1, $user2);
$userRepository = $this->createMock(UserRepository::class);
$userRepository
->expects($this->exactly(2))
->method('getByUser')
->withConsecutive(
[$user1], //we are comparing values here, not references
[$user2]
);
$userController = new UserController($userRepository);
$userController->findByUsers($user1, $user2);
}