# 理解 DataSet(數據集)和 DataTable(數據表)
PHPUnit 的數據庫擴展模塊的核心概念是 DataSet(數據集)和 DataTable(數據表)。為了掌握如何使用 PHPUnit 進行測試,需要試著去了解這些簡單的概念。DataSet(數據集)和 DataTable(數據表)是圍繞著數據庫表、行、列的抽象層。通過一套簡單的API,底層數據庫內容被隱藏在對象結構之下,同時,這個對象結構也可以用其他非數據庫數據源來實現。
為了能比較實際內容和預期內容,這種抽象是必須的。預期內容可以用諸如 XML、 YAML、 CSV 文件或者 PHP 數組等方式來表達。DataSet 和 DataTable 接口以語義相似的方式模擬關系數據庫存儲,從而能夠對這些概念上完全不同的數據源進行比較。
在測試中,數據庫斷言的工作流由以下三個簡單的步驟組成:
-
用表名稱來指定數據庫中的一個或多個表(實際上是指定了一個數據集)
-
用你喜歡的格式(YAML、XML等等)來指定預期數據集
-
斷言這兩個數據集陳述是彼此相等的。
在 PHPUnit 的數據庫擴展中,斷言并非唯一使用 DataSet 和 DataTable 的情形。就像上一節中所展示的那樣,它們也用于描述數據庫的初始內容。數據庫 TestCase 類強制要求定義一個基境數據集,隨后用它來:
-
根據此數據集所指定的所有表名,將數據庫中對應表內的行全部刪除。
-
將數據集內數據表中的所有行寫入數據庫。
### 可用的各種實現
有三種不同類型的 DataSet/DataTable:
-
基于文件的 DataSet 和 DataTable
-
基于查詢的 DataSet 和 DataTable
-
篩選與組合 DataSet 和 DataTable
基于文件的數據集和表一般用于初始化基境或描述數據庫的預期狀態。
### Flat XML DataSet (平直 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”,以及各自的值。
不過,這種簡單性是有代價的。
從上面這個例子里不太容易看出該如何指定一個空表。其實可以插入一個沒有屬性值的標簽,以空表的名字作為標簽名。空的 guestbook 表所對應的 Flat XML 文件大致上可能是這樣:
~~~
<?xml version="1.0" ?>
<dataset>
<guestbook />
</dataset>
~~~
在 Flat XML DataSet 中,要處理 NULL 值會非常煩。在幾乎所有數據庫中(Oracle 是個例外),NULL 值和空字符串值是有區別的,這一點在 Flat XML 格式中很難表述。可以在數據行的表述中省略掉對應的屬性來表示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>
~~~
在這個例子里第二個條目是匿名發表的。但是這為列的識別帶來了一個非常嚴重的問題。在數據集相等斷言的判定過程中,每個數據集都需要指明每個表擁有哪些列。如果有一個列在數據表的所有行里其值都是 NULL,那么數據庫擴展模塊又該從何得知表中包含這個列呢?
在這里,Flat XML DataSet 做了一個關鍵假設:一個表的列信息由此表第一行的屬性定義決定。在上面這個例子里,這意味著 guestbook 有 “id”、“content”、“user” 和 “created” 這幾個列。第二行中 “user” 列沒有定義,因此將向數據庫中插入 NULL 值。
如果從數據集中刪掉第一行,因為沒有指定 “user”,guestbook 表擁有的列就只剩下 “id”、“content” 和 “created”。
要在有 NULL 值的情況下有效地使用 Flat XML Dataset,就必須保證每個表的第一行不包含 NULL 值,只有后繼的那些行才能省略屬性。這就有點棘手,因為數據行的排列順序也是數據斷言的一個相關因素。
反過來,如果在 Flat XML Dataset 中只指明了實際表中所有列的某個子集,那么所有省略掉的列都會設為它們的的默認值。如果某個省略掉的列的定義是 “NOT NULL DEFAULT NULL”,就會出現錯誤。
總的來說,建議只在不需要 NULL 值的情況下使用 Flat XML Dataset。
可以在數據庫 TestCase 中調用 `createFlatXmlDataSet($filename)` 方法來創建 Flat XML Dataset 實例:
~~~
<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createFlatXmlDataSet('myFlatXmlFixture.xml');
}
}
?>
~~~
### XML DataSet (XML 數據集)
有另外一種更加結構化的 XML DataSet,它寫起來有點冗長,但是規避了 Flat XML DataSet 所存在的 NULL 問題。在根節點 `<dataset>` 內,可以指定 `<table>`、`<column>`、 `<row>`、`<value>` 和 `<null />` 標簽。和上面用 Flat XML 所定義的留言本數據集等價的 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>` 都有一個名稱,并且必須有對所有列及其名稱的定義。其下可以包含零個或任意正整數個 `<row>` 元素。沒有定義 `<row>` 意味著這是個空表。`<value>` 和 `<null />` 標簽必須按照之前給定 `<column>` 元素的順序來指定。`<null />` 標簽顯然意味著這個值為 NULL。
可以在數據庫 TestCase 中調用 `createXmlDataSet($filename)` 方法來創建 XML DataSet 實例:
~~~
<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createXMLDataSet('myXmlFixture.xml');
}
}
?>
~~~
### MySQL XML DataSet (MySQL XML 數據集)
這種新的 XML 格式是 [MySQL 數據庫服務器](http://www.mysql.com)專用的。PHPUnit 3.5 加入了對這種格式的支持。可以用 [`mysqldump`](http://dev.mysql.com/doc/refman/5.0/en/mysqldump.html) 工具來生成這種格式的文件。與同樣為 `mysqldump` 所支持的 CSV 數據集不同,這種 XML 格式可以在單個文件中包含多個表的數據。要生成這種格式的文件,可以這樣調用 `mysqldump`:
~~~
mysqldump --xml -t -u [username] --password=[password] [database] > /path/to/file.xml
~~~
可以在數據庫 TestCase 中調用 `createMySQLXMLDataSet($filename)` 方法來使用這個文件:
~~~
<?php
class MyTestCase extends PHPUnit_Extensions_Database_TestCase
{
public function getDataSet()
{
return $this->createMySQLXMLDataSet('/path/to/file.xml');
}
}
?>
~~~
### YAML DataSet (YAML 數據集)
也可以用 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: ""`。
目前,數據庫 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 數據集)
另外一種基于文件的 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 DataSet 中無法指定 NULL 值。給出一個空白列的結果是往這個列中插入數據庫的默認空值。
可以這樣創建 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 (數組數據集)
在 PHPUnit 的數據庫擴展中,(尚)沒有基于數組的 DataSet,不過很容易自行實現之。留言本的例子大致是這樣:
~~~
<?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 相比有很明顯的優點:
-
PHP 數組顯然可以處理 `NULL` 值。
-
不需要為斷言提供任何額外文件,可以直接在 TestCase 中指定。
對于這種 DataSet 而言,和平直 XML、CSV、YAML DataSet 一樣,表的列名信息由第一個指定的行的鍵名定義。在上面這個例子里,就是 “id”、“content”、“user” 和 “created”。
這個數組 DataSet 類的實現是非常簡單直接的:
~~~
<?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)數據集)
對于數據庫斷言,不僅需要有基于文件的 DataSet,同時也需要有一種內含數據庫實際內容的基于查詢/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');
?>
~~~
在關于數據庫斷言的那一節中有更多關于如何使用 Query DataSet 的細節。
### Database (DB) Dataset (數據庫數據集)
通過訪問測試所使用的數據庫連接,可以自動創建包含數據庫所有表以及其內容的 DataSet。所使用的數據庫由數據庫連接工廠方法的第二個參數指定。
可以像 `testGuestbook()` 中那樣創建整個數據庫所對應的 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 (替換數據集)
前面談到了 Flat XML 和 CSV DataSet 所存在的 NULL 問題,不過有一種稍微有點復雜的解決方法可以讓這兩種數據集都能正常處理 NULL。
Replacement DataSet 是已有數據集的修飾器(decorator),能夠將數據集中任意列的值替換為其他替代值。為了讓留言本的例子能夠處理 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 (數據集篩選器)
如果有一個非常大的基境文件,可以用數據集篩選器來為需要包含在子數據集中的表和列指定白/黑名單。與 DB DataSet 聯用來對數據集中的列進行篩選尤其方便。
~~~
<?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 (組合數據集)
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
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 的數據庫擴展模塊按照基境中所指定的順序將數據行插入到數據庫內。假如數據庫中使用了外鍵,這就意味著必須指定好表的順序,以避免外鍵約束失敗。
### 實現自有的 DataSet/DataTable
為了理解 DataSet 和 DataTable 的內部實現,讓我們來看看 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();
}
?>
~~~
這些 public 接口在數據庫 TestCase 中 `assertDataSetsEqual()` 斷言內使用,用以檢測數據集是否相等。IDataSet 中繼承自 `IteratorAggregate` 接口的 `getIterator()` 方法用于對數據集中的所有表進行迭代。逆序迭代器讓 PHPUnit 能夠按照與創建時相反的順序對所有表執行 TRUNCATE 操作,以此來保證滿足外鍵約束。
根據具體實現的不同,要采取不同的方法來將表實例添加到數據集中。例如,在所有基于文件的數據集中,表都是在構造過程中直接從源文件生成并加入數據集中,比如 `YamlDataSet`、`XmlDataSet` 和 `FlatXmlDataSet`均是如此。
數據表則由以下接口表示:
~~~
<?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` 接口的描述表結構的對象。這個對象包含如下信息:
-
表的名稱
-
表的列名數組,按照列在結果集中出現的順序排列。
-
構成主鍵的列的數組。
這個接口還包含有檢驗兩個表的元數據實例是否彼此相等的斷言,供數據集相等斷言使用。
- PHPUnit 手冊
- 1. 安裝 PHPUnit
- 需求
- PHP 檔案包 (PHAR)
- Composer
- 可選的組件包
- 2. 編寫 PHPUnit 測試
- 測試的依賴關系
- 數據供給器
- 對異常進行測試
- 對 PHP 錯誤進行測試
- 對輸出進行測試
- 錯誤相關信息的輸出
- 3. 命令行測試執行器
- 命令行選項
- 4. 基境(fixture)
- setUp() 多 tearDown() 少
- 變體
- 基境共享
- 全局狀態
- 5. 組織測試
- 用文件系統來編排測試套件
- 用 XML 配置來編排測試套件
- 6. 有風險的測試
- 無用測試
- 意外的代碼覆蓋
- 測試執行期間產生的輸出
- 測試執行時長的超時限制
- 全局狀態篡改
- 7. 未完成的測試與跳過的測試
- 未完成的測試
- 跳過測試
- 用 @requires 來跳過測試
- 8. 數據庫測試
- 數據庫測試所支持的供應商
- 數據庫測試的難點
- 數據庫測試的四個階段
- PHPUnit 數據庫測試用例的配置
- 理解 DataSet(數據集)和 DataTable(數據表)
- 數據庫連接 API
- 數據庫斷言 API
- 常見問題(FAQ)
- 9. 測試替身
- Stubs (樁件)
- 仿件對象(Mock Object)
- Prophecy
- 對特質(Trait)與抽象類進行模仿
- 對 Web 服務(Web Services)進行上樁或模仿
- 對文件系統進行模仿
- 10. 測試實踐
- 在開發過程中
- 在調試過程中
- 11. 代碼覆蓋率分析
- 用于代碼覆蓋率的軟件衡量標準
- 包含與排除文件
- 略過代碼塊
- 指明要覆蓋的方法
- 邊緣情況
- 12. 測試的其他用途
- 敏捷文檔
- 跨團隊測試
- 13. Logging (日志記錄)
- 測試結果 (XML)
- 測試結果 (TAP)
- 測試結果 (JSON)
- 代碼覆蓋率 (XML)
- 代碼覆蓋率 (TEXT)
- 14. 擴展 PHPUnit
- 從 PHPUnit_Framework_TestCase 派生子類
- 編寫自定義斷言
- 實現 PHPUnit_Framework_TestListener
- 從 PHPUnit_Extensions_TestDecorator 派生子類
- 實現 PHPUnit_Framework_Test
- A. 斷言
- assertArrayHasKey()
- assertClassHasAttribute()
- assertArraySubset()
- assertClassHasStaticAttribute()
- assertContains()
- assertContainsOnly()
- assertContainsOnlyInstancesOf()
- assertCount()
- assertEmpty()
- assertEqualXMLStructure()
- assertEquals()
- assertFalse()
- assertFileEquals()
- assertFileExists()
- assertGreaterThan()
- assertGreaterThanOrEqual()
- assertInfinite()
- assertInstanceOf()
- assertInternalType()
- assertJsonFileEqualsJsonFile()
- assertJsonStringEqualsJsonFile()
- assertJsonStringEqualsJsonString()
- assertLessThan()
- assertLessThanOrEqual()
- assertNan()
- assertNull()
- assertObjectHasAttribute()
- assertRegExp()
- assertStringMatchesFormat()
- assertStringMatchesFormatFile()
- assertSame()
- assertStringEndsWith()
- assertStringEqualsFile()
- assertStringStartsWith()
- assertThat()
- assertTrue()
- assertXmlFileEqualsXmlFile()
- assertXmlStringEqualsXmlFile()
- assertXmlStringEqualsXmlString()
- B. 標注
- @author
- @after
- @afterClass
- @backupGlobals
- @backupStaticAttributes
- @before
- @beforeClass
- @codeCoverageIgnore*
- @covers
- @coversDefaultClass
- @coversNothing
- @dataProvider
- @depends
- @expectedException
- @expectedExceptionCode
- @expectedExceptionMessage
- @expectedExceptionMessageRegExp
- @group
- @large
- @medium
- @preserveGlobalState
- @requires
- @runTestsInSeparateProcesses
- @runInSeparateProcess
- @small
- @test
- @testdox
- @ticket
- @uses
- C. XML 配置文件
- PHPUnit
- 測試套件
- 分組
- 為代碼覆蓋率包含或排除文件
- Logging (日志記錄)
- 測試監聽器
- 設定 PHP INI 設置、常量、全局變量
- 為 Selenium RC 配置瀏覽器
- D. 升級
- E. 索引
- F. 參考書目
- G. 版權