jakzal/phpunit-injector

Custom environment raises "Class \ does not exist and could not be loaded" error

kniziol opened this issue · 19 comments

While trying to use custom environment an "Class \ does not exist and could not be loaded" error is raised. More details below.

An error

Warning: class_implements(): Class \  does not exist and could not be loaded in /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/Discovery/ClassFinder.php on line 25

Stack trace

Call Stack:
    0.0009     356336   1. {main}() /var/www/application/vendor/phpunit/phpunit/phpunit:0
    0.5061     884072   2. PHPUnit\TextUI\Command::main() /var/www/application/vendor/phpunit/phpunit/phpunit:53
    (...)
    5.0771    7918968  18. Symfony\Component\DependencyInjection\ContainerBuilder->compile() /var/www/application/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php:643
    5.3498    7922728  19. Symfony\Component\DependencyInjection\Compiler\Compiler->compile() /var/www/application/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/ContainerBuilder.php:759
   12.5243   18614784  20. Zalas\Injector\PHPUnit\Symfony\Compiler\ExposeServicesForTestsPass->process() /var/www/application/vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/Compiler/Compiler.php:141
   12.5243   18614784  21. Zalas\Injector\PHPUnit\Symfony\Compiler\ExposeServicesForTestsPass->discoverServices() /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/ExposeServicesForTestsPass.php:29
   12.5243   18614784  22. Zalas\Injector\PHPUnit\Symfony\Compiler\Discovery\PropertyDiscovery->run() /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/ExposeServicesForTestsPass.php:41
   12.5243   18615160  23. Zalas\Injector\PHPUnit\Symfony\Compiler\Discovery\PropertyDiscovery->findTestCases() /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/Discovery/PropertyDiscovery.php:34
   12.5243   18615160  24. Zalas\Injector\PHPUnit\Symfony\Compiler\Discovery\ClassFinder->findImplementations() /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/Discovery/PropertyDiscovery.php:39
   12.5243   18615856  25. Zalas\Injector\PHPUnit\Symfony\Compiler\Discovery\ClassFinder->find() /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/Discovery/ClassFinder.php:26
   12.6436   18669688  26. Zalas\Injector\PHPUnit\Symfony\Compiler\Discovery\ClassFinder->findClassesInFile() /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/Discovery/ClassFinder.php:38
   12.6679   18723048  27. Zalas\Injector\PHPUnit\Symfony\Compiler\Discovery\ClassFinder->Zalas\Injector\PHPUnit\Symfony\Compiler\Discovery\{closure}() /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/Discovery/ClassFinder.php:68
   12.6679   18723048  28. class_implements() /var/www/application/vendor/zalas/phpunit-injector/src/Symfony/Compiler/Discovery/ClassFinder.php:25

A test class

class ButtonServiceWithoutCommonCssClassesTest extends KernelTestCase implements ServiceContainerTestCase
{
    use SymfonyContainer;

    /**
     * Service for buttons
     *
     * @var ButtonService
     * @inject
     */
    private $buttonService;

    // (...)

    /**
     * {@inheritdoc}
     */
    protected static function createKernel(array $options = []): KernelInterface
    {
        if (!isset($options['environment'])) {
            $options['environment'] = 'test_buttons_common_css_classes'; // <--- Custom environment is passed here
        }

        return parent::createKernel($options);
    }

//
// Trying to set environment using the bootKernel() method fails too
//
//    /**
//     * {@inheritdoc}
//     */
//    protected function setUp()
//    {
//        parent::setUp();
//
//        static::bootKernel([
//            'environment' => 'test_buttons_common_css_classes',
//        ]);
//    }
}

Main question

Are custom environments supported and how to solve this problem?

Looks like a bug. I'm pretty sure I've covered/tested custom environments. I'll have a look when I'm back from holidays (after 18th).

In the meantime, if you have time to debug this issue, I'd suggest to check why the namespace is empty (it's \). Is there a namespace in your test class?

It's very odd. ClassFinder::findImplementations() is only ever called with ServiceContainerTestCase::class. Not sure why in your case it's being called with \. The error message you're getting might be misleading. not true

Just noticed that your test class extends the KernelTestCase. Not sure if it causes the issue, but there's no point of doing so as the SymfonyContainer trait provides all you should need to create the kernel and container.

To be honest I mainly planned to use environment variables (APP_ENV etc) for configuration. It's still possible to do what you were trying to do, but it's a bit awkward imo:

    use SymfonyContainer {
        createKernel as traitCreateKernel;
    }

    protected static function createKernel(array $options = []): KernelInterface
    {
        if (!isset($options['environment'])) {
            $options['environment'] = 'footests';
        }

        return static::traitCreateKernel($options);
    }

Perhaps we need to provide a method to override options per test case so you don't have to override the createKernel method.

@kniziol Would it be possible if you created a small project that reproduces the issue? I'm not experiencing the same behaviour.

I checked again. Trying to extend the KernelTestCase and including the SymfonyContainer trait in the same time results in fatal error:

PHP Fatal error:  Symfony\Bundle\FrameworkBundle\Test\KernelTestCase and Zalas\Injector\PHPUnit\Symfony\TestCase\SymfonyContainer define the same property ($kernel) in the composition of Tests\AppBundle\FooServiceTest. However, the definition differs and is considered incompatible. Class was composed in tests/AppBundle/FooServiceTest.php

I'd say extending KernelTestCase is unsupported for now. It's unnecesary and I'm not sure if it's actually needed to support it.

As I said earlier, to override the environment in the way you tried to do is possible with:

namespace Tests\AppBundle;

use AppBundle\FooService;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\KernelInterface;
use Zalas\Injector\PHPUnit\Symfony\TestCase\SymfonyContainer;
use Zalas\Injector\PHPUnit\TestCase\ServiceContainerTestCase;

class FooServiceTest extends TestCase implements ServiceContainerTestCase
{
    use SymfonyContainer {
        createKernel as traitCreateKernel;
    }

    /**
     * @var FooService
     * @inject
     */
    private $foo;

    public function testIt()
    {
        $this->assertInstanceOf(FooService::class, $this->foo);
    }

    protected static function createKernel(array $options = []): KernelInterface
    {
        if (!isset($options['environment'])) {
            $options['environment'] = 'test_buttons_common_css_classes';
        }

        return static::traitCreateKernel($options);
    }
}

I realise it's a bit verbose. Perhaps it would be nice to expose a method with options that could be overriden in a test case.

I find it easier to define the APP_ENV environment variable either in phpunit.xml, or with zalas/phpunit-globals:

class FooServiceTest extends TestCase implements ServiceContainerTestCase
{
    use SymfonyContainer;

    /**
     * @var FooService
     * @inject
     */
    private $foo;

    /**
     * @env APP_ENV=test_buttons_common_css_classes
     */
    public function testIt()
    {
        $this->assertInstanceOf(FooService::class, $this->foo);
    }
}

Anyway, since I can't reproduce the original error I'm going to close this issue. Feel free to report it again with a code reproducer.

I've got a similar situation:

class_implements(): Class Tests\Traits\
     does not exist and could not be loaded

I suspect that it's from a test where I am using a shortened namespace path in my initial 'use' blocks: tests/App/Controller/TodoControllerTest.php:9: use Tests\Traits;,
and then inside the class I will write: use Traits\TestLog;

So, I'm using the namespace as an indication of the type of class being used - I'll do the same in regular code with Vo\Email or Entity\Profile for example.

I will try to get a very minimal set of code and repo to duplicate in the next few days.

It has, btw found a number of badly name-spaced files, with the 'Tests' part of the namespace in the wrong place or with a lower-case 't', so it's already done something useful for code cleanliness.

Thanks for reporting back!

What’s the version of phpdocumentor/reflection-docblock that was brought in as a dependency in your case?

I've got the following in my composer.lock, but I'm using the zalas-phpunit-injector-extension.phar via the extensionsDirectory entry.

  • phpdocumentor/reflection-docblock is at 4.3.0 and the other related items are
  • phpdocumentor/reflection-common 1.0.1
  • phpdocumentor/type-resolver 0.4.0

@alister these are the latest.

I haven't tried reproducing your issue with namespace aliases, but it might be environment specific.

I'll try to put a mini-project together to reproduce it over the next couple of days.

@alister I might've found the cause and solution. See the referenced PR.

@alister please test with the latest master. I think this is now fixed :)

Released v1.2.3 with this fix.

Yep, that got it with the class-loading, though with my 3.4 based tests and the ServiceInjectorTest (in namespace Tests\App;), it didn't get the SerializerInterface.

PHP Fatal error: Uncaught Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException: Service "Symfony\Component\Serializer\SerializerInterface" not found: the container inside "Zalas\Injector\Service\Injector" is a smaller service locator that only knows about the "logger" service. in .../vendor/symfony/symfony/src/Symfony/Component/DependencyInjection/ServiceLocator.php:51

Stack trace:
#0 phar://.../zalas-phpunit-injector-extension.phar/vendor/zalas/injector/src/Service/Injector.php(76): Symfony\Component\DependencyInjection\ServiceLocator->get('Symfony\\Compone...')
#1 phar://.../zalas-phpunit-injector-extension.phar/vendor/zalas/injector/src/Service/Injector.php(61): Zalas\Injector\Service\Injector->getService(Object(Symfony\Component\DependencyInjection\ServiceLocator), Object(Zalas\Injector\Service\Property))
#2 phar://.../zalas-phpunit-injector-extensio in phar://.../zalas-phpunit-injector-extension.phar/vendor/zalas/injector/src/Service/Injector.php on line 78

The Psr-Loggerinterface test in the example did work, and I could also inject one of my own aliased interface that ends up at the correct default object/service until it gets keen on trying to do something with the non-service test - You have requested a non-existent service "Tests\App\BotDetector\BotDetectorNeverBotTest". I had aliased BotDetectable to 'NeverBot - and an earlier quick test injected that in fine.

Do you extend the KernelTestCase? That’s what I haven’t been doing and intially didn’t plan to support it. In retrospect I think we need to support it though.

Another problem on my side is I mainly used this lib with Symfony 4. It needs more testing with Symfony 3.4.

Well, I have figured out about 'SerializerInterface' - I'm not using that anywhere else in my code, so it will have been removed from the container. I put my BotDetectable interface back in instead, and that's found fine.

As for the test, it's just a extension of PHPUnit\Framework\TestCase (it just adds the MockeryPHPUnitIntegration trait and a couple of other additional test-quality checks).

So, the test class itself is pretty much:

class ServiceInjectorTest extends MyTestCase implements ServiceContainerTestCase {
    use SymfonyContainer;
    /**
     * @var BotDetectable
     * @inject
     */
    private $botDetectable;
     // and also @inject LoggerInterface

and then a test with a few assertInstanceOf() to make sure the properties are set as expected. I'll take that as a win. Thanks for the fixes! 👍

@alister for Symfony 3.4 or 4.0 you need to register the ExposeServicesForTestsPass in order to make private service available in your tests.

There's an issue with importing namespaces from traits. To be fixed upstream: jakzal/injector#3

I'll close this as the original issue is now fixed. If you find any new problems, please report a new issue. Thanks!