tiny/dbunit

增强使用SQLite内存引擎的数据库测试。

dev-master 2017-11-11 20:32 UTC

This package is auto-updated.

Last update: 2019-12-16 12:00:53 UTC


README

关于

此软件包通过使用SQLite内存引擎来加速数据库测试。它负责创建和管理数据库连接,以及帮助管理模式。

建议的方法

使用SQLite内存引擎可以在同一时间拥有多个数据库(以及因此的模式),因此区分全局作用域的连接和测试用例作用域的连接是有意义的。全局连接适用于处理相对稳定的模式时的常规用途。而仅限于特定测试用例的连接适用于某种沙箱环境或当预期模式会被测试代码频繁更改时。

具有全局连接的测试用例

目前只支持单个全局连接。连接的数据库模式应在--bootstrap文件中创建,其外观如下

// ...
function bootstrapDbSchema(){
    // retrieving instances of connectors' pool and sql statements runner
    $pool = \Tiny\DbUnit\ConnectionManagement\ConnectorsPoolSingletonDecorator::getInstance();
    $sqlRunner = \Tiny\DbUnit\SqlRunners\SqlRunnerSingletonDecorator::getInstance();

    // getting PDO connection
    $sqliteInMemoryConnector = $pool->getInMemoryConnector();
    $pdo = $sqliteInMemoryConnector->getPdo();

    // running schema sql which can be a plain string with all statements or 
    // absolute filepath to the file with SQL instructions
    $sql = 'CREATE TABLE tbl (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT);';
    // $sql = realpath(__DIR__.'/schema-sqlite.sql');
    
    $sqlRunner->run($pdo, $sql);
}

bootstrapDbSchema();
// ...

测试用例将如下所示

class GlobalConnectionTest extends \Tiny\DbUnit\AbstractDbUnitTestCase
{
    // this class inherits protected property of $pdo
    // from parent \Tiny\DbUnit\AbstractDbUnitTestCase

    public function setUp(){
        // preparing a connection, please note that it should go first 
        // so parent setUp runs with connection in place
        $this->useInMemoryConnector();
        parent::setUp();
    }

    // please note no getConnection() method defined
    // it is already implemented in  \Tiny\DbUnit\AbstractDbUnitTestCase
    
    // use of PHPUnit's regular functionality to set up the data
    protected function getDataSet() {
        return new \PHPUnit_Extensions_Database_DataSet_ArrayDataSet([
            'tbl' => [
                ['id' => 1, 'value' => 'whatever'],
                ['id' => 2, 'value' => 'yet whatever']
            ]
        ]);
    }
    
    public function testRowCount(){
        $this->assertEquals(2, $this->getConnection()->getRowCount('tbl'));
    }
    
    public function testDataSetUp(){
        $this->assertTableContains(
                ['id' => 1, 'value' => 'whatever'], 
                $this->getConnection()->createQueryTable('tbl', 'SELECT * FROM tbl;'));
    }
    
    public function testQueringPdo(){
        $result = $this->pdo->query('SELECT * FROM tbl WHERE id = 2;', \PDO::FETCH_ASSOC);
        $row = $result->fetchAll()[0];
        $this->assertEquals(['id' => 2, 'value' => 'yet whatever'], $row);
    }
}

具有作用域连接的测试用例

当进行测试用例作用域的连接时,不需要特殊的引导,所有操作都在测试用例内部完成。以下示例假设存在一个在路径./sql-files/users.sql(相对于测试用例文件)上的SQL文件,其内容如下

CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, login TEXT, password_hash TEXT);
class TestCaseScopeConnectionTest extends \Tiny\DbUnit\AbstractDbUnitTestCase
{
    // setUpBeforeClass is the place to call createTestCaseConnection() method
    public static function setUpBeforeClass() {
        self::createTestCaseConnection();
        self::beforeClassSql(realpath(__DIR__.'/sql-files/users.sql'));
        parent::setUpBeforeClass(); // don't forget parent
    }
    
    protected function getDataSet() {
        return new \PHPUnit_Extensions_Database_DataSet_ArrayDataSet([
            'users' =>[
                ['id' => 1, 'login' => 'login-1', 'password_hash' => 'abcdefg'],
                ['id' => 2, 'login' => 'login-2', 'password_hash' => 'gfedcba']
            ]
        ]);
    }
    
    public function setUp(){
        // still need to say what connector to use for each test
        $this->useInMemoryConnector();
        parent::setUp();
    }
    
    public function testTableIsFilled(){
        $this->assertEquals(2, $this->getConnection()->getRowCount('users'));
    }
    
    public function testNoOtherTablesExist(){
        // again please note $this->pdo field coming from parent \Tiny\DbUnit\AbstractDbUnitTestCase
        $result = $this->pdo->query("SELECT name FROM sqlite_master WHERE type='table'  AND name NOT LIKE '%sqlite%';", \PDO::FETCH_ASSOC);
        $tables = array_column($result->fetchAll(), 'name');
        $this->assertCount(1, $tables);
        $this->assertEquals('users', $tables[0]);
    }
}

提供的API

TestCase方法

\Tiny\DbUnit\AbstractDbUnitTestCase是PHPUnit的\PHPUnit_Extensions_Database_TestCase的子类。除了配置连接和模式的目的之外,没有添加特定的测试API。

self::beforeClassSql($sql) // SQL as string or filepath to .sql

setUpBeforeClass()中用于运行需要在当前测试用例中的所有测试之前执行的SQL。

self::createTestCaseConnection();

setUpBeforeClass()中用于为当前测试用例创建单独的连接。

self::afterClassSql($sql); // SQL as string or filepath to .sql

tearDownAfterClass()中用于在当前测试用例中的所有测试之后运行SQL。

$this->useInMemoryConnector();

setUp()中用于设置每个测试与连接。

$this->runSql($sql); // SQL as string or filepath to .sql 

setUp()tearDown()或任何测试方法中用于运行SQL。

作为文件路径提供的$sql参数可以是绝对路径或相对于测试用例的相对路径。此外,它们也可以作为列表或数组提供或组合使用。因此,以下所有示例都是有效的

$this->runSql('../sample-table-schema.sql', './another-table-schema.sql');
$this->runSql(['../sample-table-schema.sql', './another-table-schema.sql']);
$this->runSql('../sample-table-schema.sql', [
    'CREATE TABLE table1 (id INTEGER);',
    './another-table-schema.sql'
]);

特殊对象

还有两个全局可访问的对象

连接池

作为单例实现

\Tiny\DbUnit\ConnectionManagement\ConnectorsPoolSingletonDecorator

+ static getInstance()

+ getInMemoryConnector()传递没有参数将始终返回相同的连接器

SQL查询执行器

作为单例实现

\Tiny\DbUnit\SqlRunners\SqlRunnerSingletonDecorator

+ static getInstance()

+ run($pdo, $sql)看起来足够自解释

优势

速度

由于SQLite内存引擎会将整个数据库保存在内存中,因此不涉及磁盘操作。一开始这可能看起来没什么不同,因为通常测试不会使用大型数据集。但实际上,每个测试方法都需要大量的截断和插入来设置和拆卸。使用SQLite内存引擎可以真正加快测试过程。

灵活性

SQLite内存使得同时拥有多个连接成为可能。每个连接相关的数据库与所有其他数据库都是独立的。这为开发时对模式进行操作提供了很大的自由度。

缺点

SQLite在SQL功能和数据类型方面有一些限制和约束。因此,如果应用程序严重依赖于SQLite不支持的功能,那么使用目标数据库进行测试似乎是一个更合适的选项。

内存数据库的模式不会在任何地方持久化,一旦连接关闭就会消失。因此,必须进行模式管理,似乎没有其他选项,只能将SQL硬编码在某个地方(或者更好)存储在文件中。基本上,要准备好没有GUI(如phpMyAdmin)来开发和维护模式。

SQLite SQL方言与其他方言略有不同。因此,将模式移植到目标数据库供应商需要一些努力。

还请注意,目前无法轻松地从SQLite内存引擎切换到任何其他引擎的限制!这个限制与SQLite本身无关,因为它是因为这个包的当前实现没有这样的功能。尽管如此,如果您决定将其用于数据库测试,强烈建议创建单个(或至少几个)抽象测试用例类,以便在需要时有一个地方可以做出这些更改。

许可

MIT