9

As far as I understand, Factory and Proxy classes are generated on the fly by the autoloader if they do not exist yet in var/generation (see: What Triggers the Generation of a Factory in Magento 2)

But why do I get this error when referencing a new factory in a unit test?

ReflectionException: Class Magento\Framework\Api\Search\SearchCriteriaBuilderFactory does not exist

[...]/vendor/magento/framework/TestFramework/Unit/Helper/ObjectManager.php:161

use Magento\Framework\Api\Search\SearchCriteriaBuilderFactory;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;

class SearchCriteriaTest extends \PHPUnit_Framework_TestCase
{
    public function testFactoryGeneration()
    {
        $searchCriteriaBuilderFactory = (new ObjectManager($this))->getObject(SearchCriteriaBuilderFactory::class);
    }
}

I am using the bootstrap file dev/tests/unit/framework/bootstrap.php.


Workarounds I found to generate the class:

  • using the real object manager (Thanks @DigitalPianism):

    \Magento\Framework\App\Bootstrap::create(BP, $_SERVER)->getObjectManager()->create('\Magento\Framework\Api\Search\SearchCrite‌​riaBuilderFactory')
    
  • run setup:di:compile (given the factory is referenced in a constructor)

But I still hope to find a clean and performant solution.

Also, not sure if related, but create() of the generated factory from the unit test object manager returns null, so I don't even have a working factory yet.

10
  • Good question indeed. Does that happen with other classes or only with Magento\Framework\Api\Search\SearchCriteriaBuilder ? Commented May 24, 2016 at 9:03
  • 1
    I tried a random core class (not api interface) and get the same error: ReflectionException: Class Magento\Bundle\Model\Sales\Order\Pdf\Items\ShipmentFactory does not exist Commented May 24, 2016 at 9:09
  • What if you try \Magento\Framework\App\Bootstrap::create(BP, $_SERVER)->getObjectManager()->create('\Magento\Framework\Api\Search\SearchCriteriaBuilderFactory'); ? Commented May 24, 2016 at 9:14
  • Interesting, that works but it does not seem right to me to instantiate the real object manager in unit tests (also made this test 10 times slower) - I hope there is another way. Commented May 24, 2016 at 9:18
  • Yep, bad idea. What if instead of getObject you call getBuilder ? That should happen directly via getObject but just to test. Commented May 24, 2016 at 10:31

3 Answers 3

7

The easiest way to deal with that is to run compilation before running tests:

bin/magento setup:di:compile

The other way is to explicitly define methods for the factory mock eg. instead of doing this:

$someFactoryMock = $this->getMockBuilder('Vendor\Module\Model\SomeFactory')
        ->disableOriginalConstructor()
        ->getMock();

Do this:

$someFactoryMock = $this->getMockBuilder('Vendor\Module\Model\SomeFactory')
        ->disableOriginalConstructor()
        ->setMethods(['create'])
        ->getMock();

At some point, I tried to deal with that by calling ObjectManager::getObject before creating mock, but this doesn't look as a clean solution. Another thing is that it didn't help - it created an object, but did not save class in var/generation. I haven't dig into this more.

1
  • 1
    I accepted this answer because mocking the factory was the most elegant solution and works even if it's not generated. Commented Jun 3, 2016 at 15:34
5

The problem originates from PHPUnit mocking library, as it cannot autoload the needed class.

If you take a look into Magento dev repo, it setups Autoloader catcher, that generates a class when it is requested. If you create similar bootstrap file in your module repository it will work quite well: https://github.com/magento/magento2/blob/develop/dev/tests/unit/framework/autoload.php

<?php
/**
 * Copyright © 2016 Magento. All rights reserved.
 * See COPYING.txt for license details.
 */
$autoloader = new \Magento\Framework\TestFramework\Unit\Autoloader\ExtensionGeneratorAutoloader(
    new \Magento\Framework\Code\Generator\Io(
        new \Magento\Framework\Filesystem\Driver\File(),
        TESTS_TEMP_DIR . '/var/generation'
    )
);
spl_autoload_register([$autoloader, 'load']);

However I would advise using a different approach, by utilizing a virtual file system, so your materialized generated classes won't break your build if generated classes interface signature changes.

composer require --dev mikey179/vfsStream

And then in your bootstrap file:

$autoloader = new \Magento\Framework\TestFramework\Unit\Autoloader\ExtensionGeneratorAutoloader(
    new \Magento\Framework\Code\Generator\Io(
        new \Magento\Framework\Filesystem\Driver\File(),
        org\bovigo\vfs\vfsStream::setup('my_generated_classes')->url()
    )
);
spl_autoload_register([$autoloader, 'load']);

I was using similar approach when created an adapter for PHPSpec https://github.com/EcomDev/phpspec-magento-di-adapter/blob/master/src/Extension.php#L98

1
  • Sounds great, I'll try it out Commented May 25, 2016 at 19:23
5

Also you may use something like this

private function getMockupFactory($instanceName)
{    
    /** Magento\Framework\TestFramework\Unit\Helper\ObjectManager */
    $objectManager = $this->objectManagerHelper;
    $factory = $this->getMockBuilder($instanceName . 'Factory')
        ->disableOriginalConstructor()
        ->setMethods(['create'])
        ->getMock();
    $factory->expects($this->any())
        ->method('create')
        ->will($this->returnCallback(function($args) use ($instanceName, $objectManager) {
            return $objectManager->getObject($instanceName, $args);
        }));
    return $factory;
}

and someWhere in code just pass

class Some {
    __constructor(
        MyFactory $myFactory
      ){}
}

 $this->objectManagerHelper->getObject(Some::class,[
    'myFactory' => $this->getMockupFactory(My::class)
 ])
2
  • Used a variation of this and it fit my use case perfectly. And I like that it's generic so now I have a go-to any time I need to mock a factory.
    – tbernard
    Commented Aug 16, 2018 at 21:13
  • Nice solution. You might want to change it's behavior a bit so you can have a $instanceName and $factoryName in case you have an interface factory that is expected to return data models. Commented Feb 5, 2019 at 9:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.