class not found when unit testing a custom module - phpunit

I'm trying to write tests for a custom module I've written on Drupal 8 and keep getting an error and at this point I'm out of ideas. Here is the error:
Error: Class 'Drupal\mypackage\Services\Config\MyClassServiceConfig' not found
The PhpUnit class is under
modules\custom\mypackage\tests\src\Unit\mypackageUserAuthTest
Here is the code
class mypackageUserAuthTest extends UnitTestCase
{
protected $user;
protected $loginService;
public function setUp()
{
parent::setUp();
$this->loginService = new LoginService();
$this->user = [
'username' => 'xxx',
'password' => 'xxx',
'deviceId' => 'xxx',
'some-token' => 'xxx'
];
}
/** #test */
public function that_we_can_authenticate_a_user()
{
$IsUserLoggedIn = $this->loginService->login($this->user['username'], $this->user['password']);
$this->assertTrue($IsUserLoggedIn);
}
Now the method login in loginService code
<?php
namespace Drupal\mypackage\Rest;
use GuzzleHttp\Exception\ClientException;
use Drupal\mypackage\Services\RestServiceFactory;
use Drupal\mypackage\Services\Config\MyClassServiceConfig;
class LoginService
{
public function login($username, $password)
{
$configs = new MyClassServiceConfig(null, "mobile", "v1");
$client = RestServiceFactory::create($configs);
try {
$response = $client->post('login', [
'json' => [
'username' => $username,
'password' => $password,
'deviceId' => 'onepiece',
],
]);
return json_decode($response->getBody(), true);
} catch (ClientException $exception) {
switch ($$exception->getResponse()->getStatusCode()) {
case 402: // This only applies to co members
throw new SubscriptionRequiredException();
case 403:
throw new BlockedAccountException();
case 409:
throw new DuplicateEmailException();
case 410:
throw new PasswordDoesNotExistException();
}
throw $exception;
}
}
}
pwd result on MyClassServiceConfig class directory
/var/www/cms/web/modules/custom/mypackage/src/Services/Config
But it seems to fail on the line $configs = new MyClassServiceConfig(null, "mobile", "v1"); with the previously mentioned error :
1) Drupal\Tests\mypackage\Unit\mypackageUserAuthTest::that_we_can_authenticate_a_user
Error: Class 'Drupal\mypackage\Services\Config\MyClassServiceConfig' not found
Btw, I'm using drupal-project structure (https://github.com/drupal-composer/drupal-project)

So I spent days checking the path but it seemed that the files were not loading so I ended up adding the custom module to autload-dev part composer.json.
"autoload": {
"classmap": [
"scripts/composer/ScriptHandler.php"
],
"files": ["load.environment.php"]
},
"autoload-dev": {
"psr-4": { "Drupal\\mypackage\\": "web/modules/custom/mypackage" }
},
Now at least it seems to load the module as I'm getting an other error related to Drupal Container
\Drupal::$container is not initialized yet. \Drupal::setContainer() must be called with a real container.

It is an old question, the same thing happened to me, as I managed to solve it in my case it was as follows:
In the comment of the class where the tests are carried out, something similar to this should go:
The #coversDefaultClass annotation must go with the namespace of the class to test.
/**
* #coversDefaultClass \Drupal\my_module\MyModuleClassName
* #group my_module
*/
class MyModuleCaseTest extends UnitTestCase {
}
Maybe it will serve someone else

Related

API Platform: How to normalize a collection of embedded entities in GraphQL?

I'm trying to make a collection of subresources selectable in GraphQL (with pagination). I'd like to be able to query:
query {
getA(id: '/api/A/1') {
aId
subresources {
totalCount
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
edges {
node {
bId
}
}
}
}
}
and get the result:
{
aId: 1,
subresources: {
"totalCount": XX,
"pageInfo": {
"endCursor": "MQ==",
"startCursor": "MA==",
"hasNextPage": true,
"hasPreviousPage": false
},
edges: [
{
node: {
bId: 11
}
},
{
node: {
bId: 12
}
},
{
node: {
bId: 13
}
}
]
}
}
I'm not using Doctrine at all- I'm using custom data providers. The problem I'm encountering is that even when I return an A entity from DataProvider::getItem() that has an array of B subresources, I get an empty array for subresources in GraphQL. I get the correct data in REST though.
I'm following the instructions given in SymfonyCasts and I found a related API Platform issue, but I'm still having no luck.
I traced through API Platform core and I think it has to do with how the entity is normalized in GraphQL. Specifically, an empty array is returned in ItemNormalizer::normalizeCollectionOfRelations(). However, there's a comment saying "to-many are handled directly by the GraphQL resolver" but I'm not sure what that refers to.
Here's the entity code.
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
#[ApiResource(
graphql: ['item_query', 'collection_query', 'create', 'update', 'delete'],
collectionOperations: ['get', 'post'],
itemOperations: ['get', 'put', 'patch', 'delete'],
normalizationContext: ['groups' => ['read']],
denormalizationContext: ['groups' => ['write']],
)]
class A {
#[ApiProperty(identifier: true)]
#[Groups(['read', 'write'])]
public ?int $aId = null,
/** #var B[] */
#[ApiProperty(readableLink: true, writableLink: true)]
#[Groups(['read', 'write'])]
public $subresources = []
}
And:
#[ApiResource(
graphql: ['item_query', 'collection_query', 'create', 'update', 'delete'],
collectionOperations: ['get', 'post'],
itemOperations: ['get', 'put', 'patch', 'delete'],
normalizationContext: ['groups' => ['read']],
denormalizationContext: ['groups' => ['write']],
)]
class B {
#[ApiProperty(identifier: true)]
#[Groups(['read', 'write'])]
public ?int $bId = null,
}
My ADataProvider:
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): A {
$bs = $this->bDataProvider->getCollection(B::class, null, []);
return new A(123, $bs);
}
My BDataProvider:
/**
* #return ArrayPaginator<B>
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): ArrayPaginator {
return ArrayPaginator::fromList([new B(11), new B(12), new B(13)]);
}
ArrayPaginator implements IteratorAggregate and PaginatorInterface.
Specifically I see this error:
{
"errors": [
{
"debugMessage": "Collection returned by the collection data provider must implement ApiPlatform\\Core\\DataProvider\\PaginatorInterface or ApiPlatform\\Core\\DataProvider\\PartialPaginatorInterface.",
"message": "Internal server error",
"extensions": {
"category": "internal"
},
"locations": [
{
"line": 29,
"column": 5
}
],
"path": [
"a",
"b"
],
"trace": [
{
"file": "/homedir/core/src/GraphQl/Resolver/Stage/SerializeStage.php",
"line": 100,
"call": "ApiPlatform\\Core\\GraphQl\\Resolver\\Stage\\SerializeStage::serializeCursorBasedPaginatedCollection(array(0), array(5), array(6))"
},
TLDR: How does one use annotations (or YAML) to make attributes that are collections of subresources selectable in GraphQL?
Any help/ideas are appreciated, thanks for reading!
Found a solution: the ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface needs to be implemented by the BDataProvider.
It gets used in the ReadStage of api platform's graphql resolver. Surprisingly, it's found nowhere in the REST resolver, so this won't get called on a REST request.
The only method that needs to be implemented is getSubresource(). My basic first implementation looks like this:
public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null) {
if ($context['collection']) {
return $this->getCollection($resourceClass, $operationName, $context);
}
$id = // get your id from $identifiers;
return $this->getItem($resourceClass, $id, $operationName, $context);
}
This isn't found in the docs unfortunately, but there are a few pulls (1, 2) open to add it.

Change subscribed events during runtime (Symfony/Doctrine ED)

Taking the example from https://symfony.com/doc/current/event_dispatcher.html
class ExceptionSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
// return the subscribed events, their methods and priorities
return [
KernelEvents::EXCEPTION => [
['processException', 10],
['logException', 0],
['notifyException', -10],
],
];
}
}
Is it correct to assume that this list can be changed during runtime?
E.g.
class ExceptionSubscriber implements EventSubscriberInterface
{
protected $someToggle = false;
public static function getSubscribedEvents()
{
if ($this->someToggle) {
return [KernelEvents::EXCEPTION => ['processException']]
}
return [
KernelEvents::EXCEPTION => [
['processException', 10],
['logException', 0],
['notifyException', -10],
],
]
}
}
Is this legit and unsubscribes logException and notifyException when I set $someToggle during runtime?
No, you cannot change dynamically what events a subscriber listen to by adding logic to the getSubscribedEvents():array method.
That method is run only during a compiler pass when the container is being built, so it will only be executed after cache is cleared.
Trying to change this at runtime will have no effect.
The practical way of doing this is to put this logic into the "work" part of the listener/subscriber:
public function processException(ExceptionEvent $event)
{
if (!$this->shouldProcessException()) {
return;
}
}
The performance hit would be very small or negligible, unless getting the value for shouldProcessException() was otherwise expensive.

Codeception: How to stub return of a function in a Symfony module

I need to write APItests for a project(on Symfony) using Codeception. My API test is functional and using Symfony+REST modules.
The Api method I'm testing uses an external service call. I just stub the response of the class method, but the stub does not work and the real class method is called.
What do I need to fix in my test code or maybe I need to use another way?
Config codeception.yml
namespace: App\Tests
paths:
tests: tests
output: tests/_output
data: tests/_data
support: tests/_support
envs: tests/_envs
actor_suffix: Tester
bootstrap: codeception_bootstrap.php
extensions:
enabled:
- Codeception\Extension\RunFailed
params:
- .env.test
Config api.suite.yml
actor: ApiTester
modules:
enabled:
- Symfony:
app_path: 'src'
environment: 'test'
- Doctrine2:
depends: Symfony
cleanup: true
- REST:
url: /api/v1/
depends: Symfony
- \App\Tests\Helper\Api
Test code /tests/api/TestCest.php
namespace App\Tests\Api;
use App\Tests\ApiTester;
class TestApiCest extends BaseApiCest
{
public function addMetrics(ApiTester $I)
{
$this->mockB2bService($I);
$I->sendPost('/request', $body);
$I->seeResponseCodeIs(201);
}
}
Code for stubing
/tests/_support/Helper/ApiHelper.php
namespace App\Tests\Helper;
use Codeception\Module;
use Symfony\Component\DependencyInjection\ContainerInterface;
class ApiHelper extends Module
{
public function getContainer(): ContainerInterface
{
/** #var Module\Symfony $symfony */
$symfony = $this->getModule('Symfony');
return $symfony->kernel->getContainer();
}
}
/api/BaseApiCest.php
namespace App\Tests\Api;
use App\Module\Service\B2BService\B2bResponse;
use App\Tests\ApiTester;
use App\Tests\Helper\ApiHelper;
use Codeception\Stub;
abstract class BaseApiCest
{
protected ApiHelper $apiHelper;
protected function _inject(ApiHelper $apiUser)
{
$this->apiHelper = $apiUser;
}
protected function mockB2bService(ApiTester $I): void
{
$container = $this->apiHelper->getContainer();
$serviceId = '\App\Module\Service\B2BService\B2bService';
$auth = Stub::make(
\App\Module\Service\B2BService\B2bService::class, [
'createSomeActive' => new B2bResponse(['success' => true, 'message' => '', 'code' => 200])
]);
$container->set($serviceId, $auth);
$I->persistPermanentService($serviceId);
}
Class I try to stub
/src/Module/Service/B2bService.php
class B2bService implements B2bServiceInterface
{
public function createSomeActive(SomeParams $params): B2bResponse
{
$response = $this->httpClient->post('/somerequest', $requestBody);
$decodedResponse = $this->jsonDecodeResponse($response);
return $decodedResponse;
//if sucsess then return ['success' => true, 'message' => '', 'code' => 200]
}
}

Testing form with CKEditor

I want to test my ArticleForm which contains CKEditor field:
$builder->add('content', CKEditorType::class, array(
'config' => array('uiColor' => '#ffffff'),
'required' => true));
However when I run PHPUnit I got the following error:
Argument 1 passed to Ivory\CKEditorBundle\Form\Type\CKEditorType::__construct()
must be an instance of Ivory\CKEditorBundle\Model\ConfigManagerInterface, none given
My test config is the same as for dev and prod where CKEditor works fine:
ivory_ck_editor:
default_config: default
configs:
default:
filebrowserBrowseRoute: elfinder
filebrowserBrowseRouteParameters: []
The test case extends Symfonys' TypeTestCase which creates its' own factory. This probably is the cause. However I don't know how to force this factory to provide the proper CKEditor instance. Does anybody know how to do it?
Problem solved with PreloadedExtension:
class ArticleTypeTest {
protected function getExtensions() {
return array(new PreloadedExtension(array($this->getCKEditor()), array()));
}
...
protected function getCKEditor() {
$configManager = $this->getMockBuilder ( ConfigManagerInterface::class )->disableOriginalConstructor ()->getMock ();
$pluginManager = $this->getMockBuilder ( PluginManagerInterface::class )->disableOriginalConstructor ()->getMock ();
$stylesSetManager = $this->getMockBuilder ( StylesSetManagerInterface::class )->disableOriginalConstructor ()->getMock ();
$templateManager = $this->getMockBuilder ( TemplateManagerInterface::class )->disableOriginalConstructor ()->getMock ();
$type = new CKEditorType($configManager, $pluginManager, $stylesSetManager, $templateManager);
return $type;
}
}

Disable Soft Deleteable filter for hard delete record doesn't work

I'm trying to disable "Soft Deleteable" filter during functional testing and I do it as follow:
First option: tried to disable at tearDown() in my test:
protected function tearDown() {
parent::tearDown();
$entityUser = $this->em->getRepository("UserSecurityBundle:User")->find($this->user->getUser()->getId());
$filter = $this->em->getFilters()->disable('softdeleteable');
$this->em->remove($entityUser);
$this->em->flush();
$this->em->close();
}
Didn't work.
Second option: make a doctrine_test.yml and import in config_test.yml:
imports:
- { resource: config.yml }
- { resource: doctrine_test.yml }
- { resource: security_test.yml }
In this case I remove the doctrine.yml and include in config_prod.yml.
Didn't work.
This is how my doctrine_test.yml section look like:
filters:
softdeleteable:
class: Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter
enabled: false
Third option: disable the filter in setUp():
public function setUp() {
static::$kernel = static::createKernel();
static::$kernel->boot();
$this->em = static::$kernel->getContainer()->get('doctrine')->getManager();
$fixture = new LoadFeeData();
$fixture->load($this->em);
$this->em->getFilters()->disable('softdeleteable');
$this->user = $this->createUser();
parent::setUp();
}
Didn't work.
Any advice? Solution?
So after some research, after doing more test I found the solution, see below:
protected function tearDown() {
parent::tearDown();
$entityAccount = $this->em->getRepository("UserSecurityBundle:Account")->find(array("id" => $this->user->getId(), "user" => $this->user->getUser()->getId()));
$entityUser = $entityAccount->getUser();
$entityCompanyHasWtax = $this->em->getRepository("ApprovalBundle:CompanyHasWtax")->findOneBy(array("company" => $this->company, "wtax" => $this->fee, "user" => $this->user->getUser()->getId()));
foreach ($this->em->getEventManager()->getListeners() as $eventName => $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof \Gedmo\SoftDeleteable\SoftDeleteableListener) {
$this->em->getEventManager()->removeEventListener($eventName, $listener);
}
}
}
$this->em->remove($entityCompanyHasWtax);
$this->em->remove($entityAccount);
$this->em->remove($entityUser);
$this->em->flush();
$this->em->close();
}
Aparently Doctrine has a bug since disabling the filter in this way:
$this->em->getFilters()->disable('softdeleteable');
Doesn't work, good look for others
Although this question is a bit old maybe it is useful to someone:
Creating your own event listener might be a better solution:
class SoftDeleteableListener extends BaseSoftDeleteableListener
{
/**
* #inheritdoc
*/
public function onFlush(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
//return from event listener if you disabled filter: $em->getFilters()->disable('softdeleteable');
if (!$om->getFilters()->isEnabled('softdeleteable')) {
return;
}
parent::onFlush($args);
}
}
And adding in your config:
gedmo.listener.softdeleteable:
class: AppBundle\EventListener\SoftDeleteableListener
tags:
- { name: doctrine.event_subscriber, connection: default }
calls:
- [ setAnnotationReader, [ #annotation_reader ] ]
source: https://github.com/Atlantic18/DoctrineExtensions/issues/1175

Resources