Gere fábrica ou proxy em testes de unidade: "ReflectionException: Class ... Factory is not exist"

8

Tanto quanto eu entendo, Factorye as Proxyclasses são geradas em tempo real pelo carregador automático, se ainda não existirem var/generation(veja: O que desencadeia a geração de uma fábrica no Magento 2 )

Mas por que recebo esse erro ao referenciar uma nova fábrica em um teste de unidade?

ReflectionException: A classe Magento \ Framework \ Api \ Search \ SearchCriteriaBuilderFactory não existe

[...] / 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);
    }
}

Estou usando o arquivo de inicialização dev/tests/unit/framework/bootstrap.php.


Soluções alternativas que encontrei para gerar a classe:

  • usando o gerenciador de objetos real (Obrigado @DigitalPianism):

    \Magento\Framework\App\Bootstrap::create(BP, $_SERVER)->getObjectManager()->create('\Magento\Framework\Api\Search\SearchCrite‌​riaBuilderFactory')
  • executar setup:di:compile(dado que a fábrica é referenciada em um construtor)

Mas ainda espero encontrar uma solução limpa e com bom desempenho.

Além disso, não tenho certeza se está relacionado, mas create()a fábrica gerada a partir do gerenciador de objetos de teste da unidade retorna null, então eu ainda não tenho uma fábrica funcionando.

Fabian Schmengler
fonte
Boa pergunta, de fato. Isso acontece com outras classes ou apenas com Magento\Framework\Api\Search\SearchCriteriaBuilder?
Raphael no Digital Pianism
1
Eu tentei uma classe principal aleatório (não interface API) e obter o mesmo erro: ReflectionException: Classe Magento \ Bundle \ Modelo \ Vendas \ Order \ Pdf \ Itens \ ShipmentFactory não existe
Fabian Schmengler
E se você tentar \Magento\Framework\App\Bootstrap::create(BP, $_SERVER)->getObjectManager()->create('\Magento\Framework\Api\Search\SearchCriteriaBuilderFactory');?
Raphael no Digital Pianism
Interessante, isso funciona, mas não me parece certo instanciar o gerenciador de objetos real em testes de unidade (também fez esse teste 10 vezes mais lento) - espero que exista outra maneira.
Fabian Schmengler
Sim, má ideia. E se, em vez de getObjectvocê ligar getBuilder? Isso deve acontecer diretamente via, getObjectmas apenas para testar.
Raphael no Digital Pianism

Respostas:

7

A maneira mais fácil de lidar com isso é executar a compilação antes de executar os testes:

bin/magento setup:di:compile

A outra maneira é definir explicitamente métodos para a simulação de fábrica, por exemplo. em vez de fazer isso:

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

Faça isso:

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

Em algum momento, tentei lidar com isso ligando ObjectManager::getObjectantes de criar simulação, mas isso não parece uma solução limpa. Outra coisa é que não ajudou - ele criou um objeto, mas não salvou a classe em var / generation. Não mergulhei nisso mais.

Wojtek Naruniec
fonte
1
Aceitei esta resposta porque zombar da fábrica era a solução mais elegante e funciona mesmo que não seja gerado.
Fabian Schmengler
5

O problema se origina da biblioteca de simulação do PHPUnit, pois não pode carregar automaticamente a classe necessária.

Se você der uma olhada no repositório de desenvolvimento Magento, ele configura o Autoloader catcher, que gera uma classe quando solicitado. Se você criar um arquivo de inicialização semelhante no repositório do módulo, ele funcionará muito bem: 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']);

No entanto, aconselho o uso de uma abordagem diferente, utilizando um sistema de arquivos virtual, para que suas classes geradas materializadas não quebrem sua compilação se a assinatura da interface das classes geradas for alterada.

composer require --dev mikey179/vfsStream

E então no seu arquivo de inicialização:

$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']);

Eu estava usando uma abordagem semelhante ao criar um adaptador para o PHPSpec https://github.com/EcomDev/phpspec-magento-di-adapter/blob/master/src/Extension.php#L98

Ivan Chepurnyi
fonte
Parece ótimo, eu vou tentar #
Fabian Schmengler 25/05
4

Além disso, você pode usar algo como isto

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;
}

e someWhere no código apenas passa

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

 $this->objectManagerHelper->getObject(Some::class,[
    'myFactory' => $this->getMockupFactory(My::class)
 ])
Eduard Melnyk
fonte
Utilizou uma variação disso e se encaixou perfeitamente no meu caso de uso. E eu gosto que é genérico, então agora eu tenho que ir sempre que precisar zombar de uma fábrica.
tbernard
Ótima solução. Você pode alterar um pouco seu comportamento para ter um $instanceNamee $factoryName, caso tenha uma fábrica de interfaces que deva retornar modelos de dados.
Giel Berkers