简体   繁体   English

如何加快PHPUnit + DBUnit测试套件的执行速度?

[英]How can I speed up my PHPUnit + DBUnit test suite execution?

I'm running in to some real speed issues with PHPUnit/DBUnit. 我正在使用PHPUnit / DBUnit来解决一些真正的速度问题。 Anything that extends PHPUnit_Extensions_Database_TestCase takes forever to run. 任何扩展PHPUnit_Extensions_Database_TestCase东西都需要永远运行。 With 189 tests, the suite takes around 8-9 minutes. 通过189次测试,该套件大约需要8-9分钟。 I was kind of hoping it would take 30 seconds at most ;-) 我有点希望最多需要30秒;-)

It looks like restoring the database to its initial state is the process that takes the time, so we've made our datasets as small as possible and limit the number of tables we require for each test case. 看起来将数据库恢复到其初始状态是花费时间的过程,因此我们使数据集尽可能小并限制每个测试用例所需的表数。 I am using fixtures and sharing as much as possible. 我正在使用灯具并尽可能地分享。

Are there any settings or modifications I can use to speed the execution up? 我可以使用任何设置或修改来加快执行速度吗? Looking at what the MySQL server is doing throughout the tests it seems that lots of truncate/inserting is happening, but surely it would be faster to pack the test data sets into temporary tables and then simply select into them for each test? 看看MySQL服务器在整个测试过程中做了什么,似乎发生了大量的截断/插入,但是将测试数据集打包到临时表中然后只需为每个测试选择它们会更快吗?

The driver I'm using is PDO/MySQL with an XML test dataset. 我正在使用的驱动程序是带有XML测试数据集的PDO / MySQL。

Upon Googling I've managed to reduce the time it takes from 10 minutes down to 1 minute. 通过谷歌搜索,我设法将所需的时间从10分钟减少到1分钟。 It turns out that changing some InnoDB configuration settings in my.ini/my.cnf will help. 事实证明,更改my.ini / my.cnf中的一些InnoDB配置设置会有所帮助。

Setting innodb_flush_log_at_trx_commit = 2 seems to do the job. 设置innodb_flush_log_at_trx_commit = 2似乎可以完成这项工作。 After you change it, restart your MySQL server. 更改后,重新启动MySQL服务器。

More on dev.mysql.com: innodb_flush_log_at_trx_commit 有关dev.mysql.com的更多信息:innodb_flush_log_at_trx_commit

The setting controls how ACID compliant the flushing of the logs is. 该设置控制ACID如何兼容日志的刷新。 The default value is 1 which is full ACID compliance which means 默认值为1,即完全ACID合规性,这意味着

the log buffer is written out to the log file at each transaction commit and the flush to disk operation is performed on the log file. 日志缓冲区在每次事务提交时写入日志文件,并在日志文件上执行刷新到磁盘操作。

With a value of 2, the following happens: 值为2时,会发生以下情况:

The log buffer is written out to the file at each commit, but the flush to disk operation is not performed on it. 日志缓冲区在每次提交时写入文件,但不对其执行刷新到磁盘操作。

The key difference here is that because the log isn't written out at every commit, an operating system crash or power outage can wipe it out. 这里的关键区别在于,由于日志不会在每次提交时写出,因此操作系统崩溃或断电都可以将其清除。 For production, stick to a value of 1. For local development with a test database, the value of 2 should be safe. 对于生产,坚持使用值1.对于使用测试数据库进行本地开发,值2应该是安全的。

If you're working with data that will be transferred to the live database, I would suggest sticking with the value of 1. 如果您正在使用将传输到实时数据库的数据,我建议坚持使用值1。

The fixture creation in DbUnit is extremely slow. DbUnit中的夹具创建非常慢。 It takes 1.5 sec every time with core2duo e8400 4gb kingston 1333. You can find the bottleneck with xdebug and fix it (if you can), or you can do one of the following: 使用core2duo e8400 4gb金士顿1333每次需要1.5秒。您可以找到xdebug的瓶颈并修复它(如果可以的话),或者您可以执行以下操作之一:

1.) 1.)

You can run only test files you currently develop with a custom bootstrap xml: 您只能使用自定义引导程序xml运行当前开发的测试文件:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://phpunit.de/phpunit.xsd"
         backupGlobals="false"
         verbose="true"
         bootstrap="test/bootstrap.php">
    <testsuites>
        <testsuite>
            <directory>test/integration</directory>
            <exclude>test/integration/database/RoleDataTest.php</exclude>
        </testsuite>
    </testsuites>
    <php>
        <env name="APPLICATION_MODE" value="test"/>
    </php>
</phpunit>

The exclude part is important here. 排除部分在这里很重要。 You can use test groups too. 您也可以使用测试组。

2.) 2.)

namespace test\integration;


abstract class AbstractTestCase extends \PHPUnit_Extensions_Database_TestCase
{
    static protected $pdo;
    static protected $connection;

    /**
     * @return \PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        if (!isset(static::$pdo)) {
            static::$pdo = new \PDO('pgsql:host=localhost;port=5432;dbname=dobra_test', 'postgres', 'inflames', array(\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION));
            static::$connection = $this->createDefaultDBConnection(static::$pdo);
        }
        return static::$connection;
    }

    /**
     * @return \PHPUnit_Extensions_Database_Operation_DatabaseOperation
     */

    static protected $fixtureSet = false;

    protected function getSetUpOperation()
    {
        $c = get_class($this;
        if (!$c::$fixtureSet) {
            $c::$fixtureSet = true;
            return \PHPUnit_Extensions_Database_Operation_Factory::CLEAN_INSERT(true);
        }
        return \PHPUnit_Extensions_Database_Operation_Factory::NONE();
    }

    static protected $dataSet;

    /**
     * @return \PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    public function getDataSet()
    {
        $c = get_class($this;
        if (!isset($c::$dataSet)) {
            $c::$dataSet = $this->createDataSet();
        }
        return $c::$dataSet;
    }

    /**
     * @return \PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    abstract protected function createDataSet();

    protected function dataSetToRows($tableName, array $ids)
    {
        $transformer = new DataSetRowsTransformer($this->getDataSet());
        $transformer->findRowsByIds($tableName, $ids);
        $transformer->cutColumnPrefix();
        return $transformer->getRows();
    }

}

You can override the TestCase. 您可以覆盖TestCase。 In this example you will use only one pdo connection by every test case (you can inject it to your code with dependency injection), by overriding setup operation you can set the fixture only once per testcase or only once for every test (depends on self:: or $cls = get_class($this); $cls:: ). 在这个例子中,你将在每个测试用例中只使用一个pdo连接(你可以通过依赖注入将它注入你的代码),通过覆盖设置操作,你可以在每个测试用例中只设置一次fixture,或者每次测试只设置一次(取决于self::$cls = get_class($this); $cls:: (PHPUnit has bad design, it creates new instance by every test call, so you have to hack with the class names to store variables per instance or per class.) By this scenario you have to write the tests to depend on eachother with @depend annotation. (PHPUnit设计不好,它会在每次测试调用时创建新实例,因此您必须使用类名来存储每个实例或每个类的变量。)在这种情况下,您必须编写测试以依赖彼此依赖@depend注解。 For example you can delete the same row you created in the previous test. 例如,您可以删除在上一个测试中创建的同一行。

By this test code 1.5 secs instead of 6 x 1.5 = 9 secs : 通过此测试代码1.5 secs而不是6 x 1.5 = 9 secs

namespace test\integration\database;

use Authorization\PermissionData;
use test\integration\AbstractTestCase;
use test\integration\ArrayDataSet;

class PermissionDataTest extends AbstractTestCase
{
    static protected $fixtureSet = false;
    static protected $dataSet;

    /** @var PermissionData */
    protected $permissionData;

    /**
     * @return \PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    public function createDataSet()
    {
        return new ArrayDataSet(array(
            'permission' => array(
                array('permission_id' => '1', 'permission_method' => 'GET', 'permission_resource' => '^/$'),
                array('permission_id' => '2', 'permission_method' => 'POST', 'permission_resource' => '^/$'),
                array('permission_id' => '3', 'permission_method' => 'DELETE', 'permission_resource' => '^/$')
            ),
            'user' => array(
                array('user_id' => '1', 'user_name' => 'Jánszky László', 'user_email' => 'a@b.d', 'user_salt' => '12435')
            ),
            'user_permission' => array(
                array('user_permission_id' => '1', 'user_id' => '1', 'permission_id' => '1'),
                array('user_permission_id' => '2', 'user_id' => '1', 'permission_id' => '2')
            ),
            'role' => array(
                array('role_id' => '1', 'role_name' => 'admin')
            ),
            'role_permission' => array(
                array('role_permission_id' => '1', 'role_id' => '1', 'permission_id' => '1')
            ),
            'permission_cache' => array(
                array('permission_cache_id' => '1', 'user_id' => '1', 'permission_id' => '1'),
                array('permission_cache_id' => '2', 'user_id' => '1', 'permission_id' => '2'),
            )
        ));
    }

    public function testReadAllShouldReturnEveryRow()
    {
        $this->assertEquals($this->permissionData->readAll(), $this->dataSetToRows('permission', array(3, 2, 1)));
    }

    /** @depends testReadAllShouldReturnEveryRow */

    public function testReadAllByRoleIdShouldReturnEveryRowRelatedToRoleId()
    {
        $this->assertEquals($this->permissionData->readAllByRoleId(1), $this->dataSetToRows('permission', array(1)));
    }

    /** @depends testReadAllByRoleIdShouldReturnEveryRowRelatedToRoleId */

    public function testReadAllByUserIdShouldReturnEveryRowRelatedToUserId()
    {
        $this->assertEquals($this->permissionData->readAllByUserId(1), $this->dataSetToRows('permission', array(2, 1)));
    }

    /** @depends testReadAllByUserIdShouldReturnEveryRowRelatedToUserId */

    public function testCreateShouldAddNewRow()
    {
        $method = 'PUT';
        $resource = '^/$';
        $createdRow = $this->permissionData->create($method, $resource);
        $this->assertTrue($createdRow['id'] > 0);
        $this->assertEquals($this->getDataSet()->getTable('permission')->getRowCount() + 1, $this->getConnection()->getRowCount('permission'));
        return $createdRow;
    }

    /** @depends testCreateShouldAddNewRow */

    public function testDeleteShouldRemoveRow(array $createdRow)
    {
        $this->permissionData->delete($createdRow['id']);
        $this->assertEquals($this->getDataSet()->getTable('permission')->getRowCount(), $this->getConnection()->getRowCount('permission'));
    }

    /** @depends testDeleteShouldRemoveRow */

    public function testDeleteShouldRemoveRowAndRelations()
    {
        $this->permissionData->delete(1);
        $this->assertEquals($this->getDataSet()->getTable('permission')->getRowCount() - 1, $this->getConnection()->getRowCount('permission'));
        $this->assertEquals($this->getDataSet()->getTable('user_permission')->getRowCount() - 1, $this->getConnection()->getRowCount('user_permission'));
        $this->assertEquals($this->getDataSet()->getTable('role_permission')->getRowCount() - 1, $this->getConnection()->getRowCount('role_permission'));
        $this->assertEquals($this->getDataSet()->getTable('permission_cache')->getRowCount() - 1, $this->getConnection()->getRowCount('permission_cache'));
    }

    public function setUp()
    {
        parent::setUp();
        $this->permissionData = new PermissionData($this->getConnection()->getConnection());
    }
}

3.) 3.)

Another solution to create the fixture only once per project, and after that use every test in transactions and rollback after every test. 另一种解决方案是每个项目只创建一次夹具,然后在每次测试后使用事务中的每个测试和回滚。 (This does not work if you have pgsql deferred code which needs commit to check the constraints.) (如果你有pgsql延迟代码需要提交检查约束,这不起作用。)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM