使用PHPUnit和ZF2 Factory

时间:2015-09-22 10:01:31

标签: php zend-framework2 phpunit

我想为我的工厂实现PHPUnit测试,该工厂调用服务。 这是我的工厂:

class FMaiAffaireServiceFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $dbAdapter = $serviceLocator->get('Zend\Db\Adapter\Adapter');

        $resultSetPrototype = new ResultSet();
        $tableGateway = new TableGateway(
            'f_affaire',
            $dbAdapter,
            null,
            $resultSetPrototype
        );
        $adapter = $tableGateway->getAdapter();
        $sql = new Sql($adapter);

        $maiAffaireTable = new FMaiAffaireTable(
            $tableGateway,
            $adapter,
            $sql
        );

        $typeaffaireService = $serviceLocator->get(
            'Intranet\Service\Model\PTypeaffaireService'
        );

        $etatAffaireService = $serviceLocator->get(
            'Intranet\Service\Model\PEtataffaireService'
        );

        $maiPrestationService = $serviceLocator->get(
            'Maintenance\Service\Model\PMaiPrestationService'
        );

        $maiAffaireService = new FMaiAffaireService(
            $maiAffaireTable,
            $typeaffaireService,
            $etatAffaireService,
            $maiPrestationService
        );

        return $maiAffaireService;
    }

广告有我的测试,但它不起作用:

class FMaiAffaireServiceFactoryTest extends \PHPUnit_Framework_TestCase
{
    public function testCreateService()
    {
        $sm = new ServiceManager();
        $factory = new FMaiAffaireServiceFactory();
        $runner = $factory->createService($sm);
    }
}

编辑:我的新测试脚本:

public function testCreateService()
    {
        $this->mockDriver = $this->getMock('Zend\Db\Adapter\Driver\DriverInterface');
        $this->mockConnection = $this->getMock('Zend\Db\Adapter\Driver\ConnectionInterface');
        $this->mockDriver->expects($this->any())->method('checkEnvironment')->will($this->returnValue(true));
        $this->mockDriver->expects($this->any())->method('getConnection')->will($this->returnValue($this->mockConnection));
        $this->mockPlatform = $this->getMock('Zend\Db\Adapter\Platform\PlatformInterface');
        $this->mockStatement = $this->getMock('Zend\Db\Adapter\Driver\StatementInterface');
        $this->mockDriver->expects($this->any())->method('createStatement')->will($this->returnValue($this->mockStatement));
        $this->adapter = new Adapter($this->mockDriver, $this->mockPlatform);
        $this->sql = new Sql($this->adapter);


        $mockTableGateway = $this->getMock('Zend\Db\TableGateway\TableGateway', array(), array(), '', false);


        $smMock = $this->getMockBuilder('Zend\ServiceManager\ServiceManager')
                       ->getMock();

        $maiPrestationTable = $this->getMockBuilder('Maintenance\Model\BDD\PMaiPrestationTable')
             ->setMethods(array())
             ->setConstructorArgs(array($mockTableGateway, $this->adapter, $this->sql))
             ->getMock();

        $smMock->expects($this->any())
            ->method('get')
            ->with('Maintenance\Service\Model\PMaiPrestationService')
            ->will($this->returnValue(new PMaiPrestationService($maiPrestationTable)));

        $etatAffaireTable = $this->getMockBuilder('Intranet\Model\BDD\PEtataffaireTable')
            ->setMethods(array())
            ->setConstructorArgs(array($mockTableGateway))
            ->getMock();

        $smMock->expects($this->any())
            ->method('get')
            ->with('Intranet\Service\Model\PEtataffaireService')
            ->will($this->returnValue(new PEtataffaireService($etatAffaireTable)));

        $typeaffaireTable = $this->getMockBuilder('Intranet\Model\BDD\PTypeaffaireTable')
            ->setMethods(array())
            ->setConstructorArgs(array($mockTableGateway))
            ->getMock();

        $smMock->expects($this->any())
            ->method('get')
            ->with('Intranet\Service\Model\PTypeaffaireService')
            ->will($this->returnValue(new PTypeaffaireService($typeaffaireTable)));

        $smMock->expects($this->any())
            ->method('get')
            ->with('Zend\Db\Adapter\Adapter')
            ->will($this->returnValue($this->adapter));

        $factory = new FMaiAffaireServiceFactory();
        $runner = $factory->createService($smMock);
        // assertions here
    }

这告诉我:get无法为Zend \ Db \ Adapter \ Adapter

获取或创建实例

编辑:这是服务:

public function createService(ServiceLocatorInterface $serviceLocator)
        {
            $dbAdapter = $serviceLocator->get('Zend\Db\Adapter\Adapter');

            $resultSetPrototype = new ResultSet();
            $tableGateway = new TableGateway(
                'f_affaire',
                $dbAdapter,
                null,
                $resultSetPrototype
            );
            $adapter = $tableGateway->getAdapter();
            $sql = new Sql($adapter);

            $maiAffaireTable = new FMaiAffaireTable(
                $tableGateway,
                $adapter,
                $sql
            );

            $typeaffaireService = $serviceLocator->get(
                'Intranet\Service\Model\PTypeaffaireService'
            );

            $etatAffaireService = $serviceLocator->get(
                'Intranet\Service\Model\PEtataffaireService'
            );

            $maiPrestationService = $serviceLocator->get(
                'Maintenance\Service\Model\PMaiPrestationService'
            );

            $maiAffaireService = new FMaiAffaireService(
                $maiAffaireTable,
                $typeaffaireService,
                $etatAffaireService,
                $maiPrestationService
            );

            return $maiAffaireService;
        }

我怎样才能让它发挥作用?

感谢。

2 个答案:

答案 0 :(得分:2)

如果要测试工厂,则无需使用实际的服务管理器。如果你这样做了,你也要测试ServiceManager类,打破规则只测试一件事。

相反,您可以直接测试工厂方法并模拟服务管理器:

class-c1 class-c2

如果是服务经理,您需要自己定义退货,而不是使用其他工厂(这也意味着要测试所有这些工厂)。

请注意,返回的对象也可能需要进行模拟。例如,您的数据库适配器。

您可以在此处找到有关PHPUnit中模拟对象的更多信息: http://code.tutsplus.com/tutorials/all-about-mocking-with-phpunit--net-27252

编辑:以下是两种可能的解决方案,用于模拟您的案例中的服务管理器:

首先,您需要模拟所有依赖项。再一次,这是一个例子!我不知道你的其他类是什么样的,所以你可能需要禁用构造函数,定义方法等。

class FMaiAffaireServiceFactoryTest extends \PHPUnit_Framework_TestCase
{

    public function testCreateService()
    {
        /** @var ServiceManager|\PHPUnit_Framework_MockObject_MockObject $smMock */
        $smMock = $this->getMockBuilder('Zend\ServiceManager\ServiceManager')
            ->getMock();
        $smMock->expects($this->any())
            ->method('get')
            ->with('Intranet\Service\Model\PTypeaffaireService')
            ->will($this->returnValue(new PTypeaffaireService()));
        // more mocked returns here

        $factory = new FMaiAffaireServiceFactory();
        $runner = $factory->createService($smMock);
        // assertions here
    }

}

第一个解决方案:通过回调,非常灵活的解决方案,可以不测试依赖关系。

这个mock不关心是否通过服务管理器等获取实例而不注入依赖项。它只是确保服务管理器mock能够返回所需类的模拟。

/** @var Adapter|\PHPUnit_Framework_MockObject_MockObject $smMock */
$adapterMock = $this->getMockBuilder('Zend\Db\Adapter\Adapter')
    ->disableOriginalConstructor()
    ->getMock();
$typeaffaireService = $this->getMock('Intranet\Service\Model\PEtataffaireService');
$etataffaireService = $this->getMock('Intranet\Service\Model\PTypeaffaireService');
$maiPrestationService = $this->getMock('Maintenance\Service\Model\PMaiPrestationService');

第二个解决方案:通过指定单个方法调用。

这是严格的解决方案,如果没有注入其中一个依赖项,或者即使在错误的时间请求实例,它也会抛出错误。

$smReturns = array(
    'Zend\Db\Adapter\Adapter' => $adapterMock,
    'Intranet\Service\Model\PTypeaffaireService' => $etataffaireService,
    'Intranet\Service\Model\PEtataffaireService' => $typeaffaireService,
    'Maintenance\Service\Model\PMaiPrestationService' => $maiPrestationService,
);

/** @var ServiceManager|\PHPUnit_Framework_MockObject_MockObject $smMock */
$smMock = $this->getMockBuilder('Zend\ServiceManager\ServiceManager')
    ->getMock();
$smMock->expects($this->any())
    ->method('get')
    ->will($this->returnCallback(function($class) use ($smReturns) {
        if(isset($smReturns[$class])) {
            return $smReturns[$class];
        } else {
            return NULL;
        }
    }));

答案 1 :(得分:0)

创建一个需要四个其他依赖项对象的真实对象的工厂应该只使用四个模拟对象。

现在查看您的工厂代码并查看第一部分和第二部分之间的区别:为FMaiAffaireService对象创建最后三个参数非常简洁:从服务管理器中获取三个对象,然后您完成。这很容易被嘲笑,即使有点重复。

但是第一个参数显然需要五个模拟对象,两个真实对象,在这些对象中模拟至少三个方法(不计算在真实对象中调用的方法的数量)。另外,最后三个参数都被实例化为具有模拟参数的真实对象。

您可以在工厂测试什么?您可以在测试中做的唯一真正的断言是工厂是否忠实于合同并交付某种类型的对象。你可以在其他地方对该对象进行单元测试,因此从工厂中获取一个充满模拟的对象并使用它做一些真正的工作并不是太有用!

坚持最简单的测试,使您的工厂代码能够通过。没有循环,没有条件,因此在一次测试中获得100%的代码覆盖率非常容易。

你的工厂应该是这样的:

public function createService(ServiceLocatorInterface $serviceLocator)
{
    $maiAffaireTable = $serviceLocator->get('WHATEVER\CLASS\KEY\YOU\THINK');
    $typeaffaireService = $serviceLocator->get('Intranet\Service\Model\PTypeaffaireService');
    $etatAffaireService = $serviceLocator->get('Intranet\Service\Model\PEtataffaireService');
    $maiPrestationService = $serviceLocator->get('Maintenance\Service\Model\PMaiPrestationService');

    $maiAffaireService = new FMaiAffaireService(
        $maiAffaireTable,
        $typeaffaireService,
        $etatAffaireService,
        $maiPrestationService
    );

    return $maiAffaireService;
}

这个想法是一个具有四个对象作为依赖关系的对象是一个复杂的野兽,而工厂应该尽可能地保持干​​净和易于理解。出于这个原因,构建maiAffaireTable对象被推送到另一个工厂,这将导致更容易测试相应工厂中的那个单一方面 - 而不是在此测试中。

你只需要五个模拟:其中四个模拟你的FMaiAffaireService对象的参数,第五个是服务管理器:

    $smMock = $this->getMockBuilder(\Zend\ServiceManager\ServiceManager::class)
        ->disableOriginalConstructor()
        ->getMock();

    $FMaiAffaireTableMock = $this->getMockBuilder(FMaiAffaireTable::class)
        ->disableOriginalConstructor()
        ->getMock();


    $PTypeaffaireServiceMock = $this->getMockBuilder(PTypeaffaireService::class)
        ->disableOriginalConstructor()
        ->getMock();

    $PEtataffaireServiceMock = $this->getMockBuilder(PEtataffaireService::class)
        ->disableOriginalConstructor()
        ->getMock();

    $PMaiPrestationServiceMock = $this->getMockBuilder(PMaiPrestationService::class)
        ->disableOriginalConstructor()
        ->getMock();

请注意,我使用包含类名的字符串切换到使用包含完全限定类名的::class静态常量,即使您使用use将类导入命名空间也是如此。这可以从PHP 5.5开始工作(与使用它相比,使用它更好:IDE的自动完成,重构时的支持......)。

现在必要的设置:唯一具有被调用方法的模拟对象是服务管理器,它应该以任意顺序发出其他模拟而不抱怨。这就是returnValueMap()的用途:

$mockMap = [
    ['WHATEVER\CLASS\KEY\YOU\THINK', $FMaiAffaireTableMock],
    ['Intranet\Service\Model\PTypeaffaireService', $PTypeaffaireServiceMock],
    ['Intranet\Service\Model\PEtataffaireService',  $PEtataffaireServiceMock],
    ['Maintenance\Service\Model\PMaiPrestationService',  $PMaiPrestationServiceMock]
];
$smMock->expects($this->any())->method('get')->will($this->returnValueMap($mockMap));

现在进行最后的测试:

$factory = new FMaiAffaireServiceFactory();
$result = $factory->createService($smMock);
$this->assertInstanceOf(FMaiAffaireService::class, $result);

就是这样:实例化服务管理器及其应发出的所有模拟,将它们放入地图数组中,并运行一次工厂方法以查看是否正在创建对象。

如果这个简单的测试不适用于您的代码,那么您在代码中做错了。在工厂本身旁边执行的唯一真实代码是创建对象的构造函数。除了将传递的参数复制到内部成员之外,此构造函数不应执行任何操作。不要访问数据库,文件系统,网络,任何东西。如果你想这样做:在实例化对象后调用方法。

请注意,我根本不关心被调用的服务管理器。工厂方法要求我传递此对象,但无论是按字母顺序对所有已配置对象进行十次,零次调用,还是随机调用:这根本不重要,它是此工厂方法的实现细节。更改调用顺序不应该破坏测试。唯一相关的是代码工作并返回正确的对象。必须配置服务管理器才能使代码运行所需的工作量。