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

Posted

技术标签:

【中文标题】如何加快 PHPUnit + DBUnit 测试套件的执行速度?【英文标题】:How can I speed up my PHPUnit + DBUnit test suite execution? 【发布时间】:2012-05-10 06:59:57 【问题描述】:

我遇到了 phpUnit/DBUnit 的一些真正的速度问题。任何扩展 PHPUnit_Extensions_Database_TestCase 的东西都需要永远运行。该套件包含 189 项测试,大约需要 8-9 分钟。我有点希望最多需要 30 秒 ;-)

看起来将数据库恢复到其初始状态是一个需要时间的过程,因此我们使数据集尽可能小,并限制每个测试用例所需的表数量。我正在使用灯具并尽可能多地分享。

我可以使用任何设置或修改来加快执行速度吗?查看 mysql 服务器在整个测试过程中所做的事情,似乎发生了很多截断/插入,但是将测试数据集打包到临时表中然后为每个测试简单地选择它们肯定会更快?

我正在使用的驱动程序是带有 XML 测试数据集的 PDO/MySQL。

【问题讨论】:

您需要衡量瓶颈在哪里。如果您可以模拟整个数据库,那么您根本不需要运行 dbunit,它可能会加快您的需求。测试应该在 10 秒内运行 - 这对于测试来说实际上是相当慢的。 既然你没有提到,你是不是尽可能的使用Fixtures和分享? 我正在使用固定装置并尽可能多地共享。有什么方法可以分析测试运行者吗? 您可以排除当前不使用的测试文件。 【参考方案1】:

通过谷歌搜索,我设法将所需时间从 10 分钟减少到 1 分钟。事实证明,更改 my.ini/my.cnf 中的一些 InnoDB 配置设置会有所帮助。

设置innodb_flush_log_at_trx_commit = 2 似乎可以完成这项工作。更改后,重新启动 MySQL 服务器。

更多关于dev.mysql.com: innodb_flush_log_at_trx_commit

该设置控制日志刷新的 ACID 兼容程度。默认值为 1,即完全符合 ACID,这意味着

在每次事务提交时将日志缓冲区写入日志文件,并在日志文件上执行刷新到磁盘操作。

值为 2 时,会发生以下情况:

日志缓冲区在每次提交时写入文件,但不会对其执行刷新到磁盘操作。

这里的关键区别在于,由于不是在每次提交时都写出日志,因此操作系统崩溃或断电可能会将其清除。对于生产环境,请使用 1。对于使用测试数据库的本地开发,值 2 应该是安全的。

如果您正在处理将传输到实时数据库的数据,我建议您坚持使用 1 的值。

【讨论】:

非常感谢 - 这也将我的时间缩短了 90%!奇怪的是,因为我的表是 MyISAM……也许是内部表? 是的,这似乎很神奇...只要确保将其放在 [mysqld] 部分(而不是其他任何地方)下。 这对 pgsql 并没有真正的帮助 :-( 顺便说一句,我认为 dbunit sux,它非常慢.. 阅读文档,了解值的差异非常重要。将值 2 用于本地开发从不用于生产。在更改值之前,运行 26 次测试大约需要 51 秒。现在大约需要 2 秒。太糟糕了,您无法更改特定数据库的设置。【参考方案2】:

在 DbUnit 中创建夹具非常慢。每次使用 core2duo e8400 4gb kingston 1333 需要 1.5 秒。您可以使用 xdebug 找到瓶颈并修复它(如果可以),或者您可以执行以下操作之一:

1.)

您只能运行您当前使用自定义引导 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>

排除部分在这里很重要。您也可以使用测试组。

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


您可以覆盖 TestCase。在此示例中,每个测试用例仅使用一个 pdo 连接(您可以使用依赖注入将其注入代码),通过覆盖设置操作,您可以为每个测试用例仅设置一次夹具或为每个测试设置一次(取决于 @ 987654323@ 或 $cls = get_class($this); $cls::)。 (PHPUnit 的设计很糟糕,它会在每次测试调用时创建新实例,因此您必须使用类名来存储每个实例或每个类的变量。)在这种情况下,您必须编写测试以使用 @987654325 相互依赖@注解。例如,您可以删除在上一个测试中创建的同一行。

通过这个测试代码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.)

另一种解决方案是每个项目只创建一次夹具,然后在事务中使用每个测试并在每次测试后回滚。 (如果您有需要提交以检查约束的 pgsql 延迟代码,这将不起作用。)

【讨论】:

以上是关于如何加快 PHPUnit + DBUnit 测试套件的执行速度?的主要内容,如果未能解决你的问题,请参考以下文章

PHPUnit 和 DBUnit - 入门 [关闭]

在 PHPUnit/DBUnit 中设置外键约束

phpunit 中的 dbunit 不会截断表

如果重写setUp和tearDown,则不会调用PHP,phpunit和dbunit - getConnection和getDataSet

为啥我应该避免使用 DbUnit 来测试 MySQL?

dbunit 性能最佳实践