使用 PHPUnit 和 ZF2 工厂

Using PHPUnit and ZF2 Factory

我想为调用服务的工厂实施 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;
        }

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

谢谢。

如果你想测试一个工厂,你不需要使用实际的服务管理器。如果这样做,您也会测试 ServiceManager class,打破一次只测试一件事的规则。

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

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
    }

}

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

请注意,returned 对象可能也需要模拟。例如你的数据库适配器。

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

编辑:在您的案例中,这里有两种模拟服务管理器的可能解决方案:

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

/** @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');

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

这个 mock 不关心是否通过服务管理器等获取实例来注入依赖项。它只是确保服务管理器 mock 能够 return 所需的 mock class.

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

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

现在查看您的工厂代码,看看第一部分和第二部分之间的区别:为 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();

请注意,即使您使用 use 将 classes 导入您的命名空间。这从 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);

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

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

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