使用PHPUnit和ZF2工厂

3

我希望为调用服务的工厂实现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
    }

这告诉我:获取无法获取或创建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个回答

2
如果你想测试一个工厂,你不需要使用实际的服务管理器。如果这样做了,你会同时测试ServiceManager类,违反了一次只测试一件事情的规则。
相反,你可以直接测试工厂的方法并模拟服务管理器:
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
    }

}

在服务经理的情况下,你需要自己定义返回值,而不是使用其他工厂(这意味着还要测试所有这些工厂)。请注意,返回的对象可能也需要进行模拟。例如,您的数据库适配器。
在这里,您可以了解有关PHPUnit中模拟对象的更多信息: http://code.tutsplus.com/tutorials/all-about-mocking-with-phpunit--net-27252 编辑:这里有两个可能的解决方案,用于在您的情况下模拟服务管理器:
首先,您需要模拟所有依赖项。再次强调,这只是一个示例!我不知道您的其他类是什么样子的,因此您可能需要禁用构造函数、定义方法等。
/** @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;
        }
    }));

第二种解决方案:通过指定单个方法调用。
这是一种严格的解决方案,如果一个依赖项没有被注入,甚至在请求实例的时间不正确时会抛出错误。
/** @var ServiceManager|\PHPUnit_Framework_MockObject_MockObject $smMock */
$smMock = $this->getMockBuilder('Zend\ServiceManager\ServiceManager')
    ->getMock();
$smMock->expects($this->at(0))
    ->method('get')
    ->with('Zend\Db\Adapter\Adapter')
    ->will($this->returnValue($adapterMock));
$smMock->expects($this->at(1))
    ->method('get')
    ->with('Intranet\Service\Model\PTypeaffaireService')
    ->will($this->returnValue($typeaffaireService));
$smMock->expects($this->at(2))
    ->method('get')
    ->with('Intranet\Service\Model\PEtataffaireService')
    ->will($this->returnValue($etataffaireService));
$smMock->expects($this->at(3))
    ->method('get')
    ->with('Maintenance\Service\Model\PMaiPrestationService')
    ->will($this->returnValue($maiPrestationService));

谢谢,但是它告诉我..."调用Zend\ServiceManager\ServiceManager::get('Zend\Db\Adapter\Adapter', true)的参数0与期望值不匹配。断言两个字符串相等失败。--- 期望 +++ 实际 @@ @@ -'Maintenance\Model\BDD\FMaiAffaireTable' +'Zend\Db\Adapter\Adapter'" - Amelie
当然会,这只是一个实现服务管理器模拟的例子,不是最终解决方案。我将编辑我的答案并添加更通用的模拟对象解决方案,但我仍建议阅读关于模拟对象的教程。在使用PHPUnit时,您经常需要它们。 - StoryTeller
我不明白放置模拟对象的顺序!我的错误每次都不同!我完全迷失了!我正在编辑我的新代码... - Amelie
别担心,模拟对象一开始可能会表现得很奇怪,但是随着时间的推移,你会喜欢上它们的。特别是因为模拟是很费力的,但可以使你的测试独立,并且如果出现问题,你会准确地知道在哪里寻找错误。 - StoryTeller

0
创建一个需要四个依赖对象的真实对象的工厂,应该只使用四个虚拟对象。
现在看看你的工厂代码,并查看第一部分和第二部分之间的区别:为FMaiAffaireService对象创建最后三个参数很好,很容易从服务管理器中获取三个对象就可以完成。即使有点重复,这也很容易被模拟。
但是第一个参数显然需要五个mock对象,两个真实对象,至少要模拟这些对象内部的三个方法(不计算在真实对象中调用的方法数量)。此外,最后三个参数都会被实例化为具有自己模拟参数的真实对象。
你可以通过工厂测试什么?你在测试中唯一能做的真正断言就是检查工厂是否遵守其协议并交付特定类型的对象。你已经在其他地方对该对象进行了单元测试,因此从工厂中获取一个充满mock的对象,然后对其进行一些实际工作并不太有用!
坚持采用最简单的测试来使你的工厂代码能够通过。没有循环,没有条件,因此在一个单一的测试中轻松获得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);

这就是它:实例化服务管理器和所有模拟它应该发出的内容,将它们放入映射数组中,并运行工厂方法一次以查看对象是否被创建。
如果这个简单的测试在你的代码中不起作用,那么你的代码肯定有问题。除了工厂本身之外,唯一真正执行的代码是所创建对象的构造函数。这个构造函数不应该做任何事情,只需将传递的参数复制到内部成员中即可。不要访问数据库、文件系统、网络或其他任何东西。如果你想这样做:在对象实例化后调用一个方法。
请注意,我并不关心服务管理器是否被调用。工厂方法要求我传递这个对象,但是它被调用十次、零次、按字母顺序配置的所有对象或随机调用都无所谓,这完全是这个工厂方法的实现细节。更改调用顺序不应该破坏测试。唯一相关的事情是代码是否正常工作并返回正确的对象。配置服务管理器是必须完成的工作量,以使代码运行。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接