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.

Fabian Schmengler
  • 65,791
  • 25
  • 187
  • 421
  • Good question indeed. Does that happen with other classes or only with Magento\Framework\Api\Search\SearchCriteriaBuilder ? – Raphael at Digital Pianism May 24 '16 at 09: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 – Fabian Schmengler May 24 '16 at 09:09
  • What if you try \Magento\Framework\App\Bootstrap::create(BP, $_SERVER)->getObjectManager()->create('\Magento\Framework\Api\Search\SearchCriteriaBuilderFactory'); ? – Raphael at Digital Pianism May 24 '16 at 09: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. – Fabian Schmengler May 24 '16 at 09:18
  • Yep, bad idea. What if instead of getObject you call getBuilder ? That should happen directly via getObject but just to test. – Raphael at Digital Pianism May 24 '16 at 10:31
  • getBuilder() is protected – Fabian Schmengler May 24 '16 at 10:36
  • Well too many mistakes here, Imma get a coffee and I'll try to help you more after that. – Raphael at Digital Pianism May 24 '16 at 10:37
  • wouldn't creating a factory class be an integration test since it's build by the framework? Did you try the Object Manager of the integration test framework? https://github.com/magento/magento2/blob/develop/dev/tests/integration/framework/Magento/TestFramework/ObjectManager.php – David Verholen May 24 '16 at 21:29
  • Good question @DavidVerholen - at the moment I moved this test to the integration test suite and it works. But I still would like to be able to unit test classes that depend on a generated factory. Maybe I have to mock it - if mocking nonexistent classes works here. – Fabian Schmengler May 24 '16 at 21:35
  • mocking nonexistent classes should work. coincidentallyI googled this yesterday and found an answer of sebastian bergmann which stated so: http://stackoverflow.com/questions/28125444/phpunit-mock-non-existing-classes – David Verholen May 24 '16 at 21:50

3 Answers3

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.

Wojtek Naruniec
  • 2,112
  • 1
  • 18
  • 39
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

Ivan Chepurnyi
  • 3,006
  • 19
  • 29
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)
 ])
Eduard Melnyk
  • 51
  • 1
  • 1
  • 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 Aug 16 '18 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. – Giel Berkers Feb 05 '19 at 09:11