理解 DataSet(數(shù)據(jù)集)和 DataTable(數(shù)據(jù)表)

2018-02-24 15:41 更新

理解 DataSet(數(shù)據(jù)集)和 DataTable(數(shù)據(jù)表)

PHPUnit 的數(shù)據(jù)庫擴展模塊的核心概念是 DataSet(數(shù)據(jù)集)和 DataTable(數(shù)據(jù)表)。為了掌握如何使用 PHPUnit 進行測試,需要試著去了解這些簡單的概念。DataSet(數(shù)據(jù)集)和 DataTable(數(shù)據(jù)表)是圍繞著數(shù)據(jù)庫表、行、列的抽象層。通過一套簡單的API,底層數(shù)據(jù)庫內容被隱藏在對象結構之下,同時,這個對象結構也可以用其他非數(shù)據(jù)庫數(shù)據(jù)源來實現(xiàn)。

為了能比較實際內容和預期內容,這種抽象是必須的。預期內容可以用諸如 XML、 YAML、 CSV 文件或者 PHP 數(shù)組等方式來表達。DataSet 和 DataTable 接口以語義相似的方式模擬關系數(shù)據(jù)庫存儲,從而能夠對這些概念上完全不同的數(shù)據(jù)源進行比較。

在測試中,數(shù)據(jù)庫斷言的工作流由以下三個簡單的步驟組成:

  • 用表名稱來指定數(shù)據(jù)庫中的一個或多個表(實際上是指定了一個數(shù)據(jù)集)

  • 用你喜歡的格式(YAML、XML等等)來指定預期數(shù)據(jù)集

  • 斷言這兩個數(shù)據(jù)集陳述是彼此相等的。

在 PHPUnit 的數(shù)據(jù)庫擴展中,斷言并非唯一使用 DataSet 和 DataTable 的情形。就像上一節(jié)中所展示的那樣,它們也用于描述數(shù)據(jù)庫的初始內容。數(shù)據(jù)庫 TestCase 類強制要求定義一個基境數(shù)據(jù)集,隨后用它來:

  • 根據(jù)此數(shù)據(jù)集所指定的所有表名,將數(shù)據(jù)庫中對應表內的行全部刪除。

  • 將數(shù)據(jù)集內數(shù)據(jù)表中的所有行寫入數(shù)據(jù)庫。

可用的各種實現(xiàn)

有三種不同類型的 DataSet/DataTable:

  • 基于文件的 DataSet 和 DataTable

  • 基于查詢的 DataSet 和 DataTable

  • 篩選與組合 DataSet 和 DataTable

基于文件的數(shù)據(jù)集和表一般用于初始化基境或描述數(shù)據(jù)庫的預期狀態(tài)。

Flat XML DataSet (平直 XML 數(shù)據(jù)集)

最常見的一種數(shù)據(jù)集名叫 Flat XML。這是一種非常簡單的 XML 格式,根節(jié)點為 <dataset>,根節(jié)點下的每個標簽就代表數(shù)據(jù)庫中的一行數(shù)據(jù)。標簽的名稱就等于表名,而每個屬性代表一個列。一個簡單的留言本應用程序的例子大致上可能是這樣:


<?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”,以及各自的值。

不過,這種簡單性是有代價的。

從上面這個例子里不太容易看出該如何指定一個空表。其實可以插入一個沒有屬性值的標簽,以空表的名字作為標簽名??盏?guestbook 表所對應的 Flat XML 文件大致上可能是這樣:


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

在 Flat XML DataSet 中,要處理 NULL 值會非常煩。在幾乎所有數(shù)據(jù)庫中(Oracle 是個例外),NULL 值和空字符串值是有區(qū)別的,這一點在 Flat XML 格式中很難表述。可以在數(shù)據(jù)行的表述中省略掉對應的屬性來表示NULL值。假定上面這個留言本通過在 user 列使用 NULL 值的方式來允許匿名留言,那么 guestbook 表的內容可能是這樣:


<?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>

在這個例子里第二個條目是匿名發(fā)表的。但是這為列的識別帶來了一個非常嚴重的問題。在數(shù)據(jù)集相等斷言的判定過程中,每個數(shù)據(jù)集都需要指明每個表擁有哪些列。如果有一個列在數(shù)據(jù)表的所有行里其值都是 NULL,那么數(shù)據(jù)庫擴展模塊又該從何得知表中包含這個列呢?

在這里,F(xiàn)lat XML DataSet 做了一個關鍵假設:一個表的列信息由此表第一行的屬性定義決定。在上面這個例子里,這意味著 guestbook 有 “id”、“content”、“user” 和 “created” 這幾個列。第二行中 “user” 列沒有定義,因此將向數(shù)據(jù)庫中插入 NULL 值。

如果從數(shù)據(jù)集中刪掉第一行,因為沒有指定 “user”,guestbook 表擁有的列就只剩下 “id”、“content” 和 “created”。

要在有 NULL 值的情況下有效地使用 Flat XML Dataset,就必須保證每個表的第一行不包含 NULL 值,只有后繼的那些行才能省略屬性。這就有點棘手,因為數(shù)據(jù)行的排列順序也是數(shù)據(jù)斷言的一個相關因素。

反過來,如果在 Flat XML Dataset 中只指明了實際表中所有列的某個子集,那么所有省略掉的列都會設為它們的的默認值。如果某個省略掉的列的定義是 “NOT NULL DEFAULT NULL”,就會出現(xiàn)錯誤。

總的來說,建議只在不需要 NULL 值的情況下使用 Flat XML Dataset。

可以在數(shù)據(jù)庫 TestCase 中調用 createFlatXmlDataSet($filename) 方法來創(chuàng)建 Flat XML Dataset 實例:

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
    }
}
?>

XML DataSet (XML 數(shù)據(jù)集)

有另外一種更加結構化的 XML DataSet,它寫起來有點冗長,但是規(guī)避了 Flat XML DataSet 所存在的 NULL 問題。在根節(jié)點 <dataset> 內,可以指定 <table>、<column>、 <row>、<value><null /> 標簽。和上面用 Flat XML 所定義的留言本數(shù)據(jù)集等價的 XML DataSet 如下:


<?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> 都有一個名稱,并且必須有對所有列及其名稱的定義。其下可以包含零個或任意正整數(shù)個 <row> 元素。沒有定義 <row> 意味著這是個空表。<value><null /> 標簽必須按照之前給定 <column> 元素的順序來指定。<null /> 標簽顯然意味著這個值為 NULL。

可以在數(shù)據(jù)庫 TestCase 中調用 createXmlDataSet($filename) 方法來創(chuàng)建 XML DataSet 實例:

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createXMLDataSet('myXmlFixture.xml');
    }
}
?>

MySQL XML DataSet (MySQL XML 數(shù)據(jù)集)

這種新的 XML 格式是 MySQL 數(shù)據(jù)庫服務器專用的。PHPUnit 3.5 加入了對這種格式的支持。可以用 mysqldump 工具來生成這種格式的文件。與同樣為 mysqldump 所支持的 CSV 數(shù)據(jù)集不同,這種 XML 格式可以在單個文件中包含多個表的數(shù)據(jù)。要生成這種格式的文件,可以這樣調用 mysqldump

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

可以在數(shù)據(jù)庫 TestCase 中調用 createMySQLXMLDataSet($filename) 方法來使用這個文件:

<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        return $this->createMySQLXMLDataSet('/path/to/file.xml');
    }
}
?>

YAML DataSet (YAML 數(shù)據(jù)集)

也可以用 YAML DataSet 來寫這個留言本的例子:


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 DataSet 所具有的 NULL 問題。在 YAML 中,只有列名而沒有指定值就表示 NULL。空白字符串則這樣指定:column1: ""。

目前,數(shù)據(jù)庫 TestCase 中沒有 YAML DataSet 的工廠方法,因此需要手工進行實例化:

<?php
class YamlGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        return new PHPUnit_Extensions_Database_DataSet_YamlDataSet(
            dirname(__FILE__)."/_files/guestbook.yml"
        );
    }
}
?>

CSV DataSet (CSV 數(shù)據(jù)集)

另外一種基于文件的 DataSet 是基于 CSV 文件的。數(shù)據(jù)集中的每個表用一個單獨的 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 DataSet 中無法指定 NULL 值。給出一個空白列的結果是往這個列中插入數(shù)據(jù)庫的默認空值。

可以這樣創(chuàng)建 CSV DataSet:

<?php
class CsvGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    protected function getDataSet()
    {
        $dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
        $dataSet->addTable('guestbook', dirname(__FILE__)."/_files/guestbook.csv");
        return $dataSet;
    }
}
?>

Array DataSe (數(shù)組數(shù)據(jù)集)

在 PHPUnit 的數(shù)據(jù)庫擴展中,(尚)沒有基于數(shù)組的 DataSet,不過很容易自行實現(xiàn)之。留言本的例子大致是這樣:

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

PHP 版本的 DataSet 相比于所有其他基于文件的 DataSet 相比有很明顯的優(yōu)點:

  • PHP 數(shù)組顯然可以處理 NULL 值。

  • 不需要為斷言提供任何額外文件,可以直接在 TestCase 中指定。

對于這種 DataSet 而言,和平直 XML、CSV、YAML DataSet 一樣,表的列名信息由第一個指定的行的鍵名定義。在上面這個例子里,就是 “id”、“content”、“user” 和 “created”。

這個數(shù)組 DataSet 類的實現(xiàn)是非常簡單直接的:

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

    /**
     * @param array $data
     */
    public function __construct(array $data)
    {
        foreach ($data AS $tableName => $rows) {
            $columns = array();
            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 (查詢(SQL)數(shù)據(jù)集)

對于數(shù)據(jù)庫斷言,不僅需要有基于文件的 DataSet,同時也需要有一種內含數(shù)據(jù)庫實際內容的基于查詢/SQL 的 DataSet。Query DataSet 在此閃亮登場:

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

單純以名稱來添加表是一種隱式地用以下查詢來定義 DataTable 的方法:

<?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');
?>

在關于數(shù)據(jù)庫斷言的那一節(jié)中有更多關于如何使用 Query DataSet 的細節(jié)。

Database (DB) Dataset (數(shù)據(jù)庫數(shù)據(jù)集)

通過訪問測試所使用的數(shù)據(jù)庫連接,可以自動創(chuàng)建包含數(shù)據(jù)庫所有表以及其內容的 DataSet。所使用的數(shù)據(jù)庫由數(shù)據(jù)庫連接工廠方法的第二個參數(shù)指定。

可以像 testGuestbook() 中那樣創(chuàng)建整個數(shù)據(jù)庫所對應的 DataSet,或者像 testFilteredGuestbook() 方法中那樣用一個白名單來將 DataSet 限制在若干表名的集合上。

<?php
class MySqlGuestbookTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @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 = array('guestbook');
        $dataSet = $this->getConnection()->createDataSet($tableNames);
        // ...
    }
}
?>

Replacement DataSet (替換數(shù)據(jù)集)

前面談到了 Flat XML 和 CSV DataSet 所存在的 NULL 問題,不過有一種稍微有點復雜的解決方法可以讓這兩種數(shù)據(jù)集都能正常處理 NULL。

Replacement DataSet 是已有數(shù)據(jù)集的修飾器(decorator),能夠將數(shù)據(jù)集中任意列的值替換為其他替代值。為了讓留言本的例子能夠處理 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
class ReplacementTest extends PHPUnit_Extensions_Database_TestCase
{
    public function getDataSet()
    {
        $ds = $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
        $rds = new PHPUnit_Extensions_Database_DataSet_ReplacementDataSet($ds);
        $rds->addFullReplacement('##NULL##', null);
        return $rds;
    }
}
?>

DataSet Filter (數(shù)據(jù)集篩選器)

如果有一個非常大的基境文件,可以用數(shù)據(jù)集篩選器來為需要包含在子數(shù)據(jù)集中的表和列指定白/黑名單。與 DB DataSet 聯(lián)用來對數(shù)據(jù)集中的列進行篩選尤其方便。

<?php
class DataSetFilterTest extends PHPUnit_Extensions_Database_TestCase
{
    public function testIncludeFilteredGuestbook()
    {
        $tableNames = array('guestbook');
        $dataSet = $this->getConnection()->createDataSet();

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

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

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

注意:不能對同一個表同時應用排除與包含兩種列篩選器,只能分別應用于不同的表。另外,表的白名單和黑名單也只能選擇其一,不能二者同時使用。

Composite DataSet (組合數(shù)據(jù)集)

Composite DataSet 能將多個已存在的數(shù)據(jù)集聚合成單個數(shù)據(jù)集,因此非常有用。如果多個數(shù)據(jù)集中存在同樣的表,其中的數(shù)據(jù)行將按照指定的順序進行追加。例如,假設有兩個數(shù)據(jù)集, 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
class CompositeTest extends PHPUnit_Extensions_Database_TestCase
{
    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;
    }
}
?>

當心外鍵

在建立基境的過程中, PHPUnit 的數(shù)據(jù)庫擴展模塊按照基境中所指定的順序將數(shù)據(jù)行插入到數(shù)據(jù)庫內。假如數(shù)據(jù)庫中使用了外鍵,這就意味著必須指定好表的順序,以避免外鍵約束失敗。

實現(xiàn)自有的 DataSet/DataTable

為了理解 DataSet 和 DataTable 的內部實現(xiàn),讓我們來看看 DataSet 的接口。如果沒打算自行實現(xiàn) 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();
}
?>

這些 public 接口在數(shù)據(jù)庫 TestCase 中 assertDataSetsEqual() 斷言內使用,用以檢測數(shù)據(jù)集是否相等。IDataSet 中繼承自 IteratorAggregate 接口的 getIterator() 方法用于對數(shù)據(jù)集中的所有表進行迭代。逆序迭代器讓 PHPUnit 能夠按照與創(chuàng)建時相反的順序對所有表執(zhí)行 TRUNCATE 操作,以此來保證滿足外鍵約束。

根據(jù)具體實現(xiàn)的不同,要采取不同的方法來將表實例添加到數(shù)據(jù)集中。例如,在所有基于文件的數(shù)據(jù)集中,表都是在構造過程中直接從源文件生成并加入數(shù)據(jù)集中,比如 YamlDataSet、XmlDataSetFlatXmlDataSet均是如此。

數(shù)據(jù)表則由以下接口表示:

<?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() 方法之外,這個接口是一目了然的。數(shù)據(jù)庫擴展模塊中的各種斷言(將于下一章中介紹)用到了所有這些方法,因此它們全部都是必需的。getTableMetaData() 方法需要返回一個實現(xiàn)了 PHPUnit_Extensions_Database_DataSet_ITableMetaData 接口的描述表結構的對象。這個對象包含如下信息:

  • 表的名稱

  • 表的列名數(shù)組,按照列在結果集中出現(xiàn)的順序排列。

  • 構成主鍵的列的數(shù)組。

這個接口還包含有檢驗兩個表的元數(shù)據(jù)實例是否彼此相等的斷言,供數(shù)據(jù)集相等斷言使用。

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號