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ù)庫。
有三種不同類型的 DataSet/DataTable:
基于文件的 DataSet 和 DataTable
基于查詢的 DataSet 和 DataTable
篩選與組合 DataSet 和 DataTable
基于文件的數(shù)據(jù)集和表一般用于初始化基境或描述數(shù)據(jù)庫的預期狀態(tài)。
最常見的一種數(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,它寫起來有點冗長,但是規(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');
}
}
?>
這種新的 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 來寫這個留言本的例子:
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"
);
}
}
?>
另外一種基于文件的 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;
}
}
?>
在 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];
}
}
?>
對于數(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é)。
通過訪問測試所使用的數(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);
// ...
}
}
?>
前面談到了 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;
}
}
?>
如果有一個非常大的基境文件,可以用數(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ù)集聚合成單個數(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ù)庫中使用了外鍵,這就意味著必須指定好表的順序,以避免外鍵約束失敗。
為了理解 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
、XmlDataSet
和 FlatXmlDataSet
均是如此。
數(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ù)集相等斷言使用。
更多建議: