非常教程

Phpunit 6参考手册

指南 | Guides

Database Testing

使用任何编程语言的许多初学者和中级单元测试示例都表明,使用简单测试来测试应用程序的逻辑非常简单。对于以数据库为中心的应用程序而言,这远离现实。例如,开始使用WordPress,TYPO3或Symfony与Doctrine或Propel,并且您将很容易遇到PHPUnit的相当多问题:仅仅因为数据库与这些库紧密耦合。

确保你已经安装了PHP扩展pdo和特定于数据库的扩展,例如pdo_mysql。 否则,下面显示的示例将不起作用。

您可能从您的日常工作和项目中了解这种情况,您希望将您的全新或经验丰富的PHPUnit技能发挥作用并受到以下问题之一的阻碍:

  1. 您要测试的方法执行一个相当大的JOIN操作,并使用这些数据来计算一些重要的结果。
  2. 您的业​​务逻辑混合使用SELECT,INSERT,UPDATE和DELETE语句。
  3. 您需要在两个以上的表(可能多得多)中设置测试数据,以获得您想要测试的方法的合理初始数据。

DbUnit扩展为测试目的大大简化了数据库的设置,并允许您在执行一系列操作后验证数据库的内容。

支持数据库测试的供应商

DbUnit目前支持MySQL,PostgreSQL,Oracle和SQLite。通过Zend Framework或Doctrine 2集成,它可以访问其他数据库系统,例如IBM DB2或Microsoft SQL Server。

数据库测试困难

所有关于单元测试的例子都不包括与数据库的交互,这是一个很好的理由:这些类型的测试对于设置和维护来说都很复杂。在对数据库进行测试时,您需要注意以下变量:

  • 数据库模式和表
  • 将测试所需的行插入这些表中
  • 测试运行后验证数据库的状态
  • 清理每个新测试的数据库

由于许多数据库API(如PDO,MySQLi或OCI8)使用起来很繁琐,而且在编写这些步骤时手动执行这些操作绝对是一场噩梦。

测试代码应尽可能简短和精确,原因如下:

  • 您不希望修改大量的测试代码,以便对生产代码进行很少的更改。
  • 您希望能够在写入测试代码后几个月轻松阅读和理解测试代码。

此外,您必须认识到,数据库本质上是您的代码的全局输入变量。测试套件中的两个测试可以针对同一个数据库运行,可能会多次重复使用数据。一次测试失败可能会轻易影响以下测试的结果,使您的测试体验变得非常困难。前面提到的清理步骤对解决“数据库是全球输入”问题具有重要意义。

DbUnit以一种优雅的方式帮助简化所有这些与数据库测试有关的问题。

与不使用数据库的测试相比,PHPUnit无法帮助您的事实是数据库测试非常慢。取决于与数据库的交互有多大,您的测试可能会花费相当长的时间。但是,如果您将每个测试所用的数据量保持较小,并尝试使用非数据库测试测试尽可能多的代码,那么即使对于大型测试套件,您也可以在一分钟内轻松离开。

Doctrine 2 project的测试套件,例如,目前拥有约1000测试中有将近一半人访问数据库,并仍然运行在与标准台式计算机上的MySQL数据库15秒的测试套件。

数据库测试的四个阶段

在关于xUnit测试模式的书中,Gerard Meszaros列出了单元测试的四个阶段:

  1. 设置夹具
  2. 测试中的运动系统
  3. 验证结果
  4. 拆除

什么是 Fixture? Fixture描述了您执行测试时应用程序和数据库所处的初始状态。

测试数据库需要至少挂钩安装和拆卸来清理并将所需的夹具数据写入表格。但是,数据库扩展有足够的理由恢复数据库测试中的四个阶段,以类似于为每个单个测试执行的以下工作流程:

1.清理型数据库

由于总是有第一个测试针对数据库运行,因此您不知道表中是否已有数据。PHPUnit将针对您指定的所有表执行TRUNCATE,以将其状态重置为空。

2.设置装置

然后,PHPUnit将遍历指定的所有灯具行并将它们插入到它们各自的表中。

3-5 运行测试,验证结果和拆解

数据库重置并加载其初始状态后,实际测试由PHPUnit执行。这部分测试代码根本不需要知道数据库扩展,您可以继续使用代码进行测试。

在你的测试中,使用一个特殊的断言来调用assertDataSetsEqual()验证,然而,这完全是可选的。该功能将在“数据库断言”部分进行说明。

配置一个PHPUnit数据库TestCase

通常,在使用PHPUnit时,您的测试用例将按以下方式扩展PHPUnit \ Framework \ TestCase类:

<?php
use PHPUnit\Framework\TestCase;

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

如果你想测试与数据库扩展一起工作的代码,那么安装程序会更复杂一点,你必须扩展一个不同的抽象TestCase,要求你实现两个抽象方法,getConnection()getDataSet()

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class MyGuestbookTest extends TestCase
{
    use TestCaseTrait;

    /**
     * @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');
    }
}
?>

Implementing getConnection()

为了允许清理和装置加载功能工作,PHPUnit数据库扩展需要访问通过PDO库抽取到各供应商的数据库连接。需要注意的是,您的应用程序不需要基于PDO来使用PHPUnit的数据库扩展,该连接仅用于清理和夹具设置。

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

“使用数据库连接”一节介绍了此接口的API以及如何充分利用它。

Implementing getDataSet()

getDataSet()方法定义了在执行每个测试之前数据库的初始状态。数据库的状态通过DataSet和DataTable的概念抽象出来,这些概念都由接口PHPUnit_Extensions_Database_DataSet_IDataSetPHPUnit_Extensions_Database_DataSet_IDataTable来表示。下一节将详细介绍这些概念是如何工作的,以及在数据库测试中使用它们的好处。

对于实现,我们只需要知道getUpdate()方法在setUp()过程中被调用一次以获取fixture数据集并将其插入到数据库中。 在这个例子中,我们使用了一个工厂方法createFlatXMLDataSet($ filename),它通过一个XML表示来表示一个数据集。

对于数据库模式(DDL):

PHPUnit假定数据库模式及其所有表,触发器,序列和视图都是在测试运行之前创建的。这意味着您作为开发人员必须在运行该套件之前确保数据库已正确设置。

有几种方法可以实现数据库测试的前提条件。

  1. 如果您使用的是持久性数据库(不是Sqlite内存),则可以使用诸如phpMyAdmin for MySQL等工具轻松设置数据库一次,并在每次测试运行时重新使用该数据库。
  2. 如果您正在使用Doctrine 2或Propel等库,则在运行测试之前,可以使用它们的API创建您需要的数据库模式。无论何时运行测试,您都可以利用PHPUnit的引导程序和配置功能执行此代码。

提示:使用您自己的抽象数据库TestCase

从前面的实现示例中,您可以很容易地看到该getConnection()方法非常静态,可以在不同的数据库测试用例中重用。此外,为了保持测试的性能良好并降低数据库开销,您可以稍微重构代码以获取应用程序的通用抽象测试用例,该测试用例仍然允许您为每个测试用例指定不同的数据夹具:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

abstract class MyApp_Tests_DatabaseTestCase extends TestCase
{
    use TestCaseTrait;

    // only instantiate pdo once for test clean-up/fixture load
    static private $pdo = null;

    // only instantiate PHPUnit_Extensions_Database_DB_IDatabaseConnection once per test
    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具有另一个令人惊叹的功能,可以使这个测试用例变得更加通用。如果您使用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>

我们现在可以修改我们的测试用例,如下所示:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

abstract class Generic_Tests_DatabaseTestCase extends TestCase
{
    use TestCaseTrait;

    // only instantiate pdo once for test clean-up/fixture load
    static private $pdo = null;

    // only instantiate PHPUnit_Extensions_Database_DB_IDatabaseConnection once per test
    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/

如果您正在开发机器上开发,可以轻松地针对不同的数据库目标运行数据库测试。如果有几个开发人员针对相同的数据库连接运行数据库测试,则由于竞争条件可能会轻松地遇到测试失败。

了解DataSets和DataTables

PHPUnit数据库扩展的核心概念是数据集和数据表。你应该尝试理解这个简单的概念来掌握PHPUnit的数据库测试。DataSet和DataTable是围绕数据库表,行和列的抽象层。一个简单的API将底层数据库内容隐藏在一个对象结构中,这也可以由其他非数据库源实现。

这个抽象是比较数据库的实际内容与预期内容的必要条件。例如,预期可以表示为XML,YAML,CSV文件或PHP数组。DataSet和DataTable接口可以比较这些概念上不同的数据源,以语义相似的方式模拟关系数据库存储。

测试中数据库断言的工作流程由三个简单步骤组成:

  • 根据表名称(实际数据集)在数据库中指定一个或多个表格
  • 以您喜欢的格式指定预期的数据集(YAML,XML,..)
  • 断言两个数据集表示法彼此相等。

断言不是PHPUnit数据库扩展中的DataSet和DataTable的唯一用例。如前一节所示,它们还描述了数据库的初始内容。您不得不通过Database TestCase定义一个夹具数据集,然后用它来:

  • 删除数据集中指定的表中的所有行。
  • 将数据表中的所有行写入数据库。

可用的实现

有三种不同类型的数据集/数据表:

  • 基于文件的数据集和数据表
  • 基于查询的数据集和数据表
  • 过滤器和组合数据集和数据表

基于文件的数据集和表格通常用于初始夹具并描述数据库的预期状态。

平面XML数据集

最常见的数据集称为Flat XML。这是一个非常简单的xml格式,其中根节点内的标签<dataset>恰好代表数据库中的一行。标签名称等于要插入行的表格,而属性表示该列。一个简单的留言簿应用程序的例子可能如下所示:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" />
</dataset>

这显然很容易编写。这里<guestbook>是表格名称,其中两行分别用四列“id”,“content”,“user”和“created”与它们各自的值插入。

但是,这种简单性需要付出代价。

从前面的例子来看,你不知道如何指定一个空表。你可以插入一个没有属性的标签和空表的名字。一个空的留言簿表的平面xml文件将如下所示:

<?xml version="1.0" ?>
<dataset>
    <guestbook />
</dataset>

用平面xml数据集处理NULL值是很乏味的。几乎任何数据库中的NULL值与空字符串值不同(Oracle是一个例外),这在平面xml格式中很难描述。您可以通过从行规范中省略属性来表示NULL的值。如果我们的留言簿允许在用户列中使用NULL值表示匿名条目,则留言簿表的假设状态可能如下所示:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" created="2010-04-26 12:14:20" />
</dataset>

在这种情况下,第二项是匿名发布的。但是,这会导致列识别的严重问题。在数据集相等断言期间,每个数据集都必须指定一个表所包含的列。如果数据表的所有行的属性为NULL,那么数据库扩展如何知道该列应该是表的一部分?

flat xml数据集现在做出了一个关键的假设,即定义表的第一个定义行上的属性定义此表的列。在前面的例子中,这意味着“id”,“content”,“user”和“created”是留言簿表的列。对于没有定义“用户”的第二行,NULL将被插入到数据库中。

当从数据集中删除第一个留言簿条目时,由于没有指定“用户”,因此“id”,“content”和“created”将成为留言簿表的列。

要在与空值相关时有效地使用Flat XML数据集,每个表的第一行不得包含任何NULL值,并且只允许连续行省略属性。这可能很尴尬,因为行的顺序是数据库断言的相关因素。

相反,如果您只在Flat XML数据集中指定表列的子集,则所有省略的值都将设置为其默认值。如果其中一个被省略的列被定义为“NOT NULL DEFAULT NULL”,这将导致错误。

总之,我只能建议使用平面XML数据集,如果你不需要NULL值。

您可以通过调用createFlatXmlDataSet($filename)方法在数据库TestCase中创建一个平坦的xml数据集实例:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class MyTestCase extends TestCase
{
    use TestCaseTrait;

    public function getDataSet()
    {
        return $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
    }
}
?>

XML DataSet

还有另一个更加结构化的XML数据集,它有点冗长,但是避免了Flat XML数据集的NULL问题。 在根节点<dataset>内,可以指定<table>,<column>,<row>,<value>和<null />标签。 以前定义的Guestbook Flat XML的等效数据集如下所示:

<?xml version="1.0" ?>
<dataset>
    <table name="guestbook">
        <column>id</column>
        <column>content</column>
        <column>user</column>
        <column>created</column>
        <row>
            <value>1</value>
            <value>Hello buddy!</value>
            <value>joe</value>
            <value>2010-04-24 17:15:23</value>
        </row>
        <row>
            <value>2</value>
            <value>I like it!</value>
            <null />
            <value>2010-04-26 12:14:20</value>
        </row>
    </table>
</dataset>

任何定义的<table>都有一个名称,并且需要使用它们的名称定义所有列。 它可以包含零个或任何嵌套的<row>元素的正数。 定义没有<row>元素意味着表是空的。 必须按照先前给出的<column>元素的顺序指定<value>和<null />标记。 显然,<null />标记表示该值为NULL。

您可以通过调用createXmlDataSet($filename)方法在数据库TestCase中创建一个xml数据集实例:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class MyTestCase extends TestCase
{
    use TestCaseTrait;

    public function getDataSet()
    {
        return $this->createXMLDataSet('myXmlFixture.xml');
    }
}
?>

MySQL XML DataSet

这种新的XML格式特定于MySQL数据库服务器。它支持PHPUnit 3.5。这种格式的文件可以使用该mysqldump实用程序生成。与mysqldump也支持的CSV数据集不同,此XML格式的单个文件可以包含多个表的数据。你可以通过调用mysqldump这样的格式来创建一个文件:

mysqldump --xml -t -u [username] --password=[password] [database] > /path/to/file.xml

该文件可以通过调用createMySQLXMLDataSet($filename)方法在数据库测试用例中使用:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class MyTestCase extends TestCase
{
    use TestCaseTrait;

    public function getDataSet()
    {
        return $this->createMySQLXMLDataSet('/path/to/file.xml');
    }
}
?>

YAML DataSet

或者,您可以将YAML数据集用于留言簿示例:

guestbook:
  -
    id: 1
    content: "Hello buddy!"
    user: "joe"
    created: 2010-04-24 17:15:23
  -
    id: 2
    content: "I like it!"
    user:
    created: 2010-04-26 12:14:20

这很简单,容易,它解决了相似的Flat XML数据集所具有的NULL问题。YAML中的NULL只是没有指定值的列名称。一个空字符串被指定为column1: ""

YAML数据集目前在数据库TestCase上没有工厂方法,因此您必须手动实例化它:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\DbUnit\DataSet\YamlDataSet;

class YamlGuestbookTest extends TestCase
{
    use TestCaseTrait;

    protected function getDataSet()
    {
        return new YamlDataSet(dirname(__FILE__)."/_files/guestbook.yml");
    }
}
?>

CSV DataSet

另一个基于文件的数据集基于CSV文件。数据集的每个表格都表示为一个CSV文件。对于我们的留言板示例,我们将定义一个guestbook-table.csv文件:

id,content,user,created
1,"Hello buddy!","joe","2010-04-24 17:15:23"
2,"I like it!","nancy","2010-04-26 12:14:20"

虽然这对使用Excel或OpenOffice进行编辑非常方便,但您无法使用CSV数据集指定NULL值。空列会导致数据库默认值被插入到列中。

您可以通过调用以下命令来创建CSV数据集:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\DbUnit\DataSet\CsvDataSet;

class CsvGuestbookTest extends TestCase
{
    use TestCaseTrait;

    protected function getDataSet()
    {
        $dataSet = new CsvDataSet();
        $dataSet->addTable('guestbook', dirname(__FILE__)."/_files/guestbook.csv");
        return $dataSet;
    }
}
?>

Array DataSet

PHPUnit的数据库扩展中没有基于数组的数据集(但是),但我们可以轻松实现自己的数据集。我们的留言簿示例应该如下所示:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class ArrayGuestbookTest extends TestCase
{
    use TestCaseTrait;

    protected function getDataSet()
    {
        return new MyApp_DbUnit_ArrayDataSet(
            [
                'guestbook' => [
                    [
                        'id' => 1,
                        'content' => 'Hello buddy!',
                        'user' => 'joe',
                        'created' => '2010-04-24 17:15:23'
                    ],
                    [
                        'id' => 2,
                        'content' => 'I like it!',
                        'user' => null,
                        'created' => '2010-04-26 12:14:20'
                    ],
                ],
            ]
        );
    }
}
?>

与所有其他基于文件的数据集相比,PHP数据集具有明显的优势:

  • PHP数组显然可以处理NULL值。
  • 您不需要额外的断言文件,并可以直接在TestCase中指定它们。

对于像Flat XML,CSV和YAML DataSet这样的数据集,第一个指定行的键定义了表的列名,在前一种情况下,这将是“id”,“content”,“user”和“created”。

这个Array DataSet的实现非常简单明了:

<?php
class MyApp_DbUnit_ArrayDataSet extends PHPUnit_Extensions_Database_DataSet_AbstractDataSet
{
    /**
     * @var array
     */
    protected $tables = [];

    /**
     * @param array $data
     */
    public function __construct(array $data)
    {
        foreach ($data AS $tableName => $rows) {
            $columns = [];
            if (isset($rows[0])) {
                $columns = array_keys($rows[0]);
            }

            $metaData = new PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
            $table = new PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);

            foreach ($rows AS $row) {
                $table->addRow($row);
            }
            $this->tables[$tableName] = $table;
        }
    }

    protected function createIterator($reverse = false)
    {
        return new PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
    }

    public function getTable($tableName)
    {
        if (!isset($this->tables[$tableName])) {
            throw new InvalidArgumentException("$tableName is not a table in the current database.");
        }

        return $this->tables[$tableName];
    }
}
?>

Query (SQL) DataSet

对于数据库断言,不仅需要基于文件的数据集,还需要包含数据库实际内容的基于Query / SQL的数据集。这是查询数据集发光的地方:

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook');
?>

仅通过名称添加表格是用以下查询定义数据表的隐式方法:

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook', 'SELECT * FROM guestbook');
?>

您可以通过为表指定任意查询来使用此操作,例如限制行,列或添加ORDER BY子句:

<?php
$ds = new PHPUnit_Extensions_Database_DataSet_QueryDataSet($this->getConnection());
$ds->addTable('guestbook', 'SELECT id, content FROM guestbook ORDER BY created DESC');
?>

数据库断言部分将显示更多关于如何使用查询数据集的详细信息。

Database (DB) Dataset

访问测试连接可以自动创建一个DataSet,该DataSet由数据库中的所有表格组成,并指定为Connections Factory方法的第二个参数。

您可以创建完整数据库的数据集,如testGuestbook()所示,或者将其限制为一组具有白名单的指定表名称,如testFilteredGuestbook()方法所示。

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class MySqlGuestbookTest extends TestCase
{
    use TestCaseTrait;

    /**
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    public function getConnection()
    {
        $database = 'my_database';
        $user = 'my_user';
        $password = 'my_password';
        $pdo = new PDO('mysql:...', $user, $password);
        return $this->createDefaultDBConnection($pdo, $database);
    }

    public function testGuestbook()
    {
        $dataSet = $this->getConnection()->createDataSet();
        // ...
    }

    public function testFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet($tableNames);
        // ...
    }
}
?>

Replacement DataSet

我一直在讨论Flat XML和CSV DataSet的NULL问题,但是要获得这两种类型的数据集都使用NULL,有一个稍微复杂的解决方法。

Replacement DataSet是现有数据集的装饰器,允许您用另一个替换值替换数据集的任何列中的值。为了让我们的留言簿例子处理NULL值,我们指定了这样的文件:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" />
</dataset>

然后,我们将Flat XML DataSet包装到Replacement DataSet中:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class ReplacementTest extends TestCase
{
    use TestCaseTrait;

    public function getDataSet()
    {
        $ds = $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
        $rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds);
        $rds->addFullReplacement('##NULL##', null);
        return $rds;
    }
}
?>

DataSet Filter

如果您有一个较大的夹具文件,则可以使用DataSet过滤器对应包含在子数据集中的表和列进行白名单和黑名单。与DB数据集组合使用可以过滤数据集的列,这特别方便。

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class DataSetFilterTest extends TestCase
{
    use TestCaseTrait;

    public function testIncludeFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet();

        $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
        $filterDataSet->addIncludeTables(['guestbook']);
        $filterDataSet->setIncludeColumnsForTable('guestbook', ['id', 'content']);
        // ..
    }

    public function testExcludeFilteredGuestbook()
    {
        $tableNames = ['guestbook'];
        $dataSet = $this->getConnection()->createDataSet();

        $filterDataSet = new PHPUnit_Extensions_Database_DataSet_DataSetFilter($dataSet);
        $filterDataSet->addExcludeTables(['foo', 'bar', 'baz']); // only keep the guestbook table!
        $filterDataSet->setExcludeColumnsForTable('guestbook', ['user', 'created']);
        // ..
    }
}
?>

注意不能在同一张表上同时使用排除和包含列过滤,只能在不同的表上使用。另外,只能将表格加上白名单或黑名单,而不是两者。

Composite DataSet

复合数据集对于将几个已经存在的数据集合到单个数据集中非常有用。当几个数据集包含同一个表时,行按照指定的顺序附加。例如,如果我们有两个数据集fixture1.xml

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
</dataset>

又如fixture2.xml:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="2" content="I like it!" user="##NULL##" created="2010-04-26 12:14:20" />
</dataset>

使用Composite DataSet,我们可以聚合两个夹具文件:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class CompositeTest extends TestCase
{
    use TestCaseTrait;

    public function getDataSet()
    {
        $ds1 = $this->createFlatXmlDataSet('fixture1.xml');
        $ds2 = $this->createFlatXmlDataSet('fixture2.xml');

        $compositeDs = new PHPUnit_Extensions_Database_DataSet_CompositeDataSet();
        $compositeDs->addDataSet($ds1);
        $compositeDs->addDataSet($ds2);

        return $compositeDs;
    }
}
?>

注意外键

在Fixture SetUp过程中,PHPUnit的数据库扩展插入数据库中行的顺序,它们在您的fixture中指定。如果您的数据库模式使用外键,这意味着您必须按不会导致外键约束失败的顺序指定表。

实现自己的DataSets / DataTables

要理解DataSet和DataTables的内部,我们来看看DataSet的接口。如果您不打算实现自己的DataSet或DataTable,则可以跳过此部分。

<?php
interface PHPUnit_Extensions_Database_DataSet_IDataSet extends IteratorAggregate
{
    public function getTableNames();
    public function getTableMetaData($tableName);
    public function getTable($tableName);
    public function assertEquals(PHPUnit_Extensions_Database_DataSet_IDataSet $other);

    public function getReverseIterator();
}
?>

公共接口由Database TestCase上的assertDataSetsEqual()断言在内部使用,以检查数据集质量。 从IteratorAggregate接口,IDataSet继承getIterator()方法遍历数据集的所有表。 反向迭代器允许PHPUnit按照它们创建的顺序来截断表,以满足外键约束。

根据具体实施,采取不同的方法将表实例添加到数据集。例如,在所有基于文件的数据集(如YamlDataSetXmlDataSetFlatXmlDataSet)中,从源文件构建期间内部表会添加表。

一张表也由以下接口表示:

<?php
interface PHPUnit_Extensions_Database_DataSet_ITable
{
    public function getTableMetaData();
    public function getRowCount();
    public function getValue($row, $column);
    public function getRow($row);
    public function assertEquals(PHPUnit_Extensions_Database_DataSet_ITable $other);
}
?>

除getTableMetaData()方法外,它非常自我解释。 所使用的方法都是数据库扩展的不同断言所需的,这些断言将在下一章中介绍。 getTableMetaData()方法必须返回描述表结构的PHPUnit_Extensions_Database_DataSet_ITableMetaData接口的实现。 它拥有以下信息:

  • 表名
  • 表的列名称数组,按照它们出现在结果集中的顺序进行排序。
  • 主键列的数组。

该接口还有一个断言,用于检查Table Metadata的两个实例是否彼此相等,由数据集相等断言使用。

连接API

在Connection接口上有三个有趣的方法必须从getConnection()Database TestCase上的方法返回:

<?php
interface PHPUnit_Extensions_Database_DB_IDatabaseConnection
{
    public function createDataSet(Array $tableNames = NULL);
    public function createQueryTable($resultName, $sql);
    public function getRowCount($tableName, $whereClause = NULL);

    // ...
}
?>
  1. createDataSet()方法按照DataSet实现部分中的描述创建数据库(DB)数据集。<?php use PHPUnit\Framework\TestCase; use PHPUnit\DbUnit\TestCaseTrait; class ConnectionTest extends TestCase { use TestCaseTrait; public function testCreateDataSet() { $tableNames = ['guestbook']; $dataSet = $this->getConnection()->createDataSet(); } } ?>
  2. createQueryTable()方法可用于创建QueryTable的实例,为它们提供结果名称和SQL查询。 当涉及到结果/表断言时,这是一个方便的方法,这将在Database Assertions API的下一节中介绍。 <?php use PHPUnit\Framework\TestCase; use PHPUnit\DbUnit\TestCaseTrait; class ConnectionTest extends TestCase { use TestCaseTrait; public function testCreateQueryTable() { $tableNames = ['guestbook']; $queryTable = $this->getConnection()->createQueryTable('guestbook', 'SELECT \* FROM guestbook'); } } ?>
  3. getRowCount()方法是访问表中行数的一种便捷方式,可以通过附加的where子句进行筛选。 这可以用于简单的相等断言:<?php use PHPUnit\Framework\TestCase; use PHPUnit\DbUnit\TestCaseTrait; class ConnectionTest extends TestCase { use TestCaseTrait; public function testGetRowCount() { $this->assertEquals(2, $this->getConnection()->getRowCount('guestbook')); } } ?>

数据库断言API

对于测试工具,Database Extension肯定会提供一些断言,您可以使用这些断言来验证数据库的当前状态,表格和表格的行数。本节详细介绍这一功能:

声明表的行数

检查一个表是否包含特定数量的行通常很有帮助。您可以使用Connection API轻松实现此功能,无需额外的胶水代码。假设我们想检查一下,在我们的留言簿插入一行后,我们不仅有两个初始条目在前面的所有例子中都伴随着我们,而是第三个:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class GuestbookTest extends TestCase
{
    use TestCaseTrait;

    public function testAddEntry()
    {
        $this->assertEquals(2, $this->getConnection()->getRowCount('guestbook'), "Pre-Condition");

        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $this->assertEquals(3, $this->getConnection()->getRowCount('guestbook'), "Inserting failed");
    }
}
?>

声明表的状态

前面的断言很有帮助,但我们肯定希望检查表的实际内容,以验证所有值都写入了正确的列。这可以通过表格断言来实现。

为此,我们将定义一个查询表实例,该实例从表名和SQL查询中导出其内容,并将其与基于文件/数组的数据集进行比较:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class GuestbookTest extends TestCase
{
    use TestCaseTrait;

    public function testAddEntry()
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );
        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>

现在我们必须为这个断言编写expectedBook.xml Flat XML文件:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" created="2010-04-24 17:15:23" />
    <guestbook id="2" content="I like it!" user="nancy" created="2010-04-26 12:14:20" />
    <guestbook id="3" content="Hello world!" user="suzy" created="2010-05-01 21:47:08" />
</dataset>

这个断言只会传到宇宙的一秒钟,在2010-05-01 21:47:08。日期是数据库测试的一个特殊问题,我们可以通过忽略断言中的“已创建”列来规避失败。

调整后的expectedBook.xml Flat XML文件可能需要如下所示进行断言传递:

<?xml version="1.0" ?>
<dataset>
    <guestbook id="1" content="Hello buddy!" user="joe" />
    <guestbook id="2" content="I like it!" user="nancy" />
    <guestbook id="3" content="Hello world!" user="suzy" />
</dataset>

我们必须修复查询表调用:

<?php
$queryTable = $this->getConnection()->createQueryTable(
    'guestbook', 'SELECT id, content, user FROM guestbook'
);
?>

断言查询的结果

您还可以使用查询表方法断言复杂查询的结果,只需使用查询指定结果名称并将其与数据集进行比较即可:

<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\DbUnit\TestCaseTrait;

class ComplexQueryTest extends TestCase
{
    use TestCaseTrait;

    public function testComplexQuery()
    {
        $queryTable = $this->getConnection()->createQueryTable(
            'myComplexQuery', 'SELECT complexQuery...'
        );
        $expectedTable = $this->createFlatXmlDataSet("complexQueryAssertion.xml")
                              ->getTable("myComplexQuery");
        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>

声明多个表的状态

当然,您可以一次声明多个表的状态,并将查询数据集与基于文件的数据集进行比较。DataSet断言有两种不同的方式。

  1. 你可以使用Connection中的数据库(DB)数据集并将其与基于文件的数据集进行比较。<?php use PHPUnit\Framework\TestCase; use PHPUnit\DbUnit\TestCaseTrait; class DataSetAssertionsTest extends TestCase { use TestCaseTrait; public function testCreateDataSetAssertion() { $dataSet = $this->getConnection()->createDataSet(['guestbook']); $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml'); $this->assertDataSetsEqual($expectedDataSet, $dataSet); } } ?>
  2. 你可以自己构造DataSet: <?php use PHPUnit\Framework\TestCase; use PHPUnit\DbUnit\TestCaseTrait; class DataSetAssertionsTest extends TestCase { use TestCaseTrait; public function testManualDataSetAssertion() { $dataSet = new PHPUnit\_Extensions\_Database\_DataSet\_QueryDataSet(); $dataSet->addTable('guestbook', 'SELECT id, content, user FROM guestbook'); // additional tables $expectedDataSet = $this->createFlatXmlDataSet('guestbook.xml'); $this->assertDataSetsEqual($expectedDataSet, $dataSet); } } ?>

经常提及的问题

PHPUnit会(重新)为每个测试创建数据库模式吗?

不,PHPUnit要求所有数据库对象在套件启动时可用。在运行测试套件之前,必须创建数据库,表格,序列,触发器和视图。

Doctrine 2或eZ Components具有强大的工具,允许您从预定义的数据结构创建数据库模式。但是,这些必须被挂钩到PHPUnit扩展中,以允许在完整的测试套件运行之前重新创建自动数据库。

由于每个测试都完全清理数据库,因此您甚至不需要为每次测试运行重新创建数据库。永久可用的数据库完美地工作。

我是否需要在我的应用程序中使用PDO才能使数据库扩展工作?

不,PDO仅用于夹具的清洁和设置以及断言。您可以在自己的代码中使用任何想要的数据库抽象。

当我收到“连接太多”错误时,我该怎么办?

如果您不缓存从TestCase getConnection()方法创建的PDO实例,则每次数据库测试时,数据库连接的数量会增加一个或多个。使用默认配置,MySql只允许100个并发连接,其他供应商也有最大连接限制。

SubSection“使用您自己的抽象数据库TestCase”显示了如何通过在所有测试中使用单个缓存的PDO实例来防止发生此错误。

如何使用Flat XML / CSV数据集处理NULL?

不要这样做。相反,您应该使用XML或YAML数据集。

Phpunit 6

PHPUnit 是一个 xUnit 的体系结构的 PHP 单元测试框架。

主页 https://phpunit.de/
源码 https://github.com/sebastianbergmann/phpunit
版本 6
发布版本 6.4

Phpunit 6目录

1.指南 | Guides
2.注释 | Annotations
3.声明 | Assertions