牛骨文教育服务平台(让学习变的简单)

PHPUnit 数据库测试用例的配置

一般而言,使用 PHPUnit 时,测试用例都是按如下方式扩展自 PHPUnit_Framework_TestCase 类:

<?php
class MyTest extends PHPUnit_Framework_TestCase
{
    public function testCalculate()
    {
        $this->assertEquals(2, 1 + 1);
    }
}
?>

如果测试代码用到了数据库扩展模块,那么建立的过程就会更复杂一些,需要扩展另一个抽象 TestCase 类,它要求实现两个抽象方法,getConnection()getDataSet()

<?php
class MyGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        $pdo = new PDO("sqlite::memory:");
        return $this->createDefaultDBConnection($pdo, ":memory:");
    }

    /**
     * @return PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    public function getDataSet()
    {
        return $this->createFlatXMLDataSet(dirname(__FILE__)."/_files/guestbook-seed.xml");
    }
}
?>

实现 getConnection()

为了让清理与载入基境的功能正常运作,PHPUnit 数据库扩展模块需要用 PDO 库来实现跨供应商抽象访问数据库连接。重要的是要注意到,使用 PHPUnit 的数据库扩展模块并不要求应用程序本身基于PDO,PDO连接仅仅用于清理和建立基境。

在之前的例子里,我们在内存中创建 Sqlite 数据库并建立了连接,将此连接传递给 createDefaultDBConnection 方法,这个方法将 PDO 实例和第二参数(数据库名)包装在一个非常简单的数据库连接抽象层中,这个抽象层的类型是 PHPUnit_Extensions_Database_DB_IDatabaseConnection

“使用数据库连接”一节解说了这个接口的API以及如何充分利用它们。

实现 getDataSet()

getDataSet() 方法定义了在每个测试执行之前的数据库初始状态应该是什么样。数据库的状态通过由 PHPUnit_Extensions_Database_DataSet_IDataSet 所代表的 DataSet(数据集)和由 PHPUnit_Extensions_Database_DataSet_IDataTable所代表的 DataTable(数据表)这两个概念进行抽象。下一节将详细讲述这些概念是如何运作的以及在数据库测试中使用它们有什么好处。

对于具体实现,只需要知道 setUp() 中会调用一次 getDataSet() 方法来接收基境数据集并将其插入数据库。在范例中使用了工厂方法 createFlatXMLDataSet($filename),它代表一个用 XML 表示的数据集。

数据库构架(DDL)怎么办?

PHPUnit 假设在测试运行之前数据库以及其中的所有表(table)、触发器(trigger)、序列(Sequence)和视图(view)都已经创建好。这意味着开发者必须在运行测试套件之前确保数据库已经正确建立。

有几种方法来达成这个数据库测试的先决条件。

  1. 如果使用的是持久化数据库(不是 Sqlite Memory),可以很轻松地用 phpMyAdmin(针对MySQL)之类的工具来一次性建立数据库,并在每个测试中复用这个数据库。

  2. 如果使用的是诸如 Doctrine 2Propel 这样的库,可以用它们的API来在测试运行前一次性建立所需的数据库。可以利用 PHPUnit 的引导和配置 功能来在每次测试运行时执行这些代码。

小建议:使用你自己的抽象数据库 TestCase 类

从前面的实现范例中容易发现 getConnection() 方法是相当稳定的,可以在不同的数据库测试用例中重用。另外,为了保持测试的性能良好和数据库的开销较低,可以对代码进行一点重构,来为应用程序形成一个通用的抽象测试用例,同时依然可以为每个具体测试用例指定不同的数据基境:

<?php
abstract class MyApp_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
    // 只实例化 pdo 一次,供测试的清理和装载基境使用
    static private $pdo = null;

    // 对于每个测试,只实例化 PHPUnit_Extensions_Database_DB_IDatabaseConnection 一次
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO("sqlite::memory:");
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, ":memory:");
        }

        return $this->conn;
    }
}
?>

这个例子里,数据库连接信息硬编码在 PDO 连接里了。PHPUnit 有另外一个绝妙的特性,可以让这个 TestCase 类更加通用。通过 XML 配置 可以为每个测试单独配置数据库连接信息。首先,在应用程序的 tests/ 目录下创建 “phpunit.xml” 文件,内容大体是这样:


<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
    <php>
        <var name="DB_DSN" value="mysql:dbname=myguestbook;host=localhost" />
        <var name="DB_USER" value="user" />
        <var name="DB_PASSWD" value="passwd" />
        <var name="DB_DBNAME" value="myguestbook" />
    </php>
</phpunit>

现在可以修改 TestCase 类了,像这样:

<?php
abstract class Generic_Tests_DatabaseTestCase extends PHPUnit_Extensions_Database_TestCase
{
    // 只实例化 pdo 一次,供测试的清理和装载基境使用
    static private $pdo = null;

    // 对于每个测试,只实例化 PHPUnit_Extensions_Database_DB_IDatabaseConnection 一次
    private $conn = null;

    final public function getConnection()
    {
        if ($this->conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO( $GLOBALS["DB_DSN"], $GLOBALS["DB_USER"], $GLOBALS["DB_PASSWD"] );
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS["DB_DBNAME"]);
        }

        return $this->conn;
    }
}
?>

现在可以从命令行界面以不同的配置来运行数据库测试套件了:

user@desktop> phpunit --configuration developer-a.xml MyTests/
user@desktop> phpunit --configuration developer-b.xml MyTests/

在开发机上进行开发时能够轻松的针对不同的目标数据库来运行数据库测试显得非常重要。如果多个开发人员在同一个数据库连接上运行数据库测试,很容易因为竞态而导致测试失败。