PHP ZF2 Unit Tests dispatch method very slow
Asked Answered
P

1

6

I need to test a big site written in ZF2. There is 443 test and about 10000 assertions. Testing with code coverage takes 6 hours! I think I found the problem: In controller's tests I use a dispatch method from AbstractHttpControllerTestCase. Time of execution of dispatch method is increasing after each test (from fractions of second to tens of seconds).

I use ZF 2.1.3, PHPUnit 3.7, PHP_CodeCoverage 1.2, Xdebug v2.2.1, PHP 5.4.7.

My dispatch method:

public function dispatch($url, $method = HttpRequest::METHOD_GET, $params = array())
{
    $s = microtime(true);

    parent::dispatch($url, $method, $params);

    $end = microtime(true) - $s;
    echo 'dis: '.$end."\n";

    return $this->getApplication()->getMvcEvent()->getResult();
}

parent::dispatch is method from AbstractHttpControllerTestCase.

Sample of test:

$result = $this->dispatch('/archive/finance/older');

$this->assertControllerName('skycontent\controller\article');
$this->assertActionName('archive');
$this->assertParamValue('older', true);
$this->assertParamValue('category', 'finance');

$vars = (array) $result->getVariables();

$this->assertArrayHasKey('archivePosts', $vars);

Please help. Thanks.


Update:

I use process isolation and tests done in about 15 minutes (without code coverage) but I get error in the test that are marked as skipped:

PHPUnit_Framework_Exception: PHP Fatal error:  Uncaught exception 'Exception' with message 'Serialization of 'Closure' is not allowed' in -:44
Propagandize answered 13/3, 2013 at 10:44 Comment(6)
Have you tried to run each test in isolation? And dispatch() is in AbstractControllerTestCase (Zend\Test\PHPUnit\Controller)Segura
It works! Much faster, about 15 minutes, but I get error in the test that are skipped:Propagandize
PHPUnit_Framework_Exception: PHP Fatal error: Uncaught exception 'Exception' with message 'Serialization of 'Closure' is not allowed' in -:44 Stack trace: #0 -(44): serialize(Array) #1 -(348): __phpunit_run_isolated_test() #2 {main}Propagandize
Edit your question instead (place a line at the bottom and then the updated information). The error I know, I think this has to do with storing the code-coverage data but I'm not totally sure. There must be an issue about it on the PHPUnit github page. Edit: Maybe this helps? Symfony 2 + Doctrine 2 + PHPUnit 3.5: Serialization of closure exceptionSegura
Disabling backupGlobals not working.Propagandize
As outlined in that answer, you need to find out which closure is the culprit and then exclude it from serializing. Otherwise you might want to turn the bug-report bugs.php.net/bug.php?id=49516 into a feature request.Segura
B
2

Both Zend\ServiceManager and Zend\EventManager make heavy use of closures. You cannot serialize an entire application instance and expect that to work, since it would basically mean that you attempt to serialize service factories and event listeners defined as closures.

The solution may be to use a test Bootstrap.php like the one of DoctrineORMModule, which doesn't keep an application instance in memory. Here's a simplified example:

require_once __DIR__ . '/../vendor/autoload.php';

$appConfig = require __DIR__ . '/TestConfiguration.php';

\YourModuleTest\Util\ServiceManagerFactory::setConfig($appConfig);

unset($appConfig);

(the TestConfiguration should look like a standard mvc application's config)

You also need the ServiceManagerFactory. Sample implementation can be found here and here.

namespace YourModuleTest\Util;

class ServiceManagerFactory
{
    /**
     * @var array
     */
    protected static $config;

    /**
     * @param array $config
     */
    public static function setConfig(array $config)
    {
        static::$config = $config;
    }

    /**
     * Builds a new service manager
     * Emulates {@see \Zend\Mvc\Application::init()}
     */
    public static function getServiceManager()
    {
        $serviceManager = new ServiceManager(new ServiceManagerConfig(
            isset(static::$config['service_manager']) 
                ? static::$config['service_manager'] 
                : array()
        ));

        $serviceManager->setService('ApplicationConfig', static::$config);
        $serviceManager->setFactory(
            'ServiceListener',
            'Zend\Mvc\Service\ServiceListenerFactory'
        );

        /** @var $moduleManager \Zend\ModuleManager\ModuleManager */
        $moduleManager = $serviceManager->get('ModuleManager');
        $moduleManager->loadModules();

        return $serviceManager;
    }
}

Now, wherever you want in your tests, you can:

$serviceManager = \YourModuleTest\Util\ServiceManagerFactory::getServiceManager();
$application    = $serviceManager->get('Application');

$application->bootstrap();

// ...

With this setup, you can run tests in insulation.

On the other side, you should focus on real unit tests first, since ZF2 really simplifies how you can compose complex objects. You should also correctly setup the coverage filters so that unrelated code is not handled (that may eat up a lot of time).

Also, re-using the mvc application instance is wrong, since the helpers are not stateless, which makes it hard to reuse them.

Bayadere answered 13/3, 2013 at 15:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.