Prev Next

第9章 データベースのテスト

作成中のソフトウェアのテストを書いているうちに、 データベースに関するコードをテストする必要が出てくることもあるでしょう。 データベース拡張を使用すると、 データベースを特定の状態にしてからデータベース関連のコードを実行し、 データベースのデータが期待通りになっているかどうかを確かめることができます。

データベース関連のユニットテストを作成するもっとも手っ取り早い方法は、 PHPUnit_Extensions_Database_TestCase クラスを継承することです。 このクラスには、データベース接続を作成したり データベースにデータを送信したり、 テストの実行後にデータベースの中身を様々な形式のデータセットと比較したりする機能があります。 例 9.1 に、getConnection()getDataSet() の実装例を示します。

例 9.1: データベーステストケースの準備

<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';

class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getConnection()
{
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
return $this->createDefaultDBConnection($pdo, 'testdb');
}

protected function getDataSet()
{
return $this->createFlatXMLDataSet(dirname(__FILE__).'/_files/bank-account-seed.xml');
}
}
?>

getConnection() メソッドは、 PHPUnit_Extensions_Database_DB_IDatabaseConnection インターフェイスの実装を返す必要があります。 createDefaultDBConnection() メソッドを使用して、データベース接続を返すことができます。 このメソッドの最初のパラメータには PDO オブジェクトを渡し、2 番目のパラメータにはテスト対象のスキーマの名前を渡します。

getDataSet() メソッドは、 PHPUnit_Extensions_Database_DataSet_IDataSet インターフェイスの実装を返す必要があります。 現在、PHPUnit では 3 種類のデータセットが使用できます。 データセットについては 「データセット」 で説明します。

表9.1 データベーステストケースのメソッド

メソッド 意味
PHPUnit_Extensions_Database_DB_IDatabaseConnection getConnection() データベース接続を返すように実装します。これを用いて、期待するデータセットやテーブルを調べます。
PHPUnit_Extensions_Database_DataSet_IDataSet getDataSet() データセットを返すように実装します。データベースの初期設定 (setup) や後始末 (teardown) の際にこれを使用します。
PHPUnit_Extensions_Database_Operation_DatabaseOperation getSetUpOperation() オーバーライドして、各テストの最初に実行する特定の操作を返すようにします。操作の詳細については 「操作」 を参照ください。
PHPUnit_Extensions_Database_Operation_DatabaseOperation getTearDownOperation() オーバーライドして、各テストの最後に実行する特定の操作を返すようにします。操作の詳細については 「操作」 を参照ください。
PHPUnit_Extensions_Database_DB_DefaultDatabaseConnection createDefaultDBConnection(PDO $connection, string $schema) $connection PDO オブジェクトのデータベース接続ラッパーを返します。テスト対象のデータベーススキーマを $schema で指定します。このメソッドの結果を getConnection() の返り値として使用することができます。
PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet createFlatXMLDataSet(string $xmlFile) $xmlFile で指定した絶対パスにある XML ファイルから作成したフラット XML データセットを返します。XML ファイルについての詳細は 「Flat XML データセット」 を参照ください。このメソッドの結果を getDataSet() の返り値として使用することができます。
PHPUnit_Extensions_Database_DataSet_XmlDataSet createXMLDataSet(string $xmlFile) $xmlFile で指定した絶対パスにある XML ファイルから作成した XML データセットを返します。XML ファイルについての詳細は 「XML データセット」 を参照ください。このメソッドの結果を getDataSet() の返り値として使用することができます。
void assertTablesEqual(PHPUnit_Extensions_Database_DataSet_ITable $expected, PHPUnit_Extensions_Database_DataSet_ITable $actual) $expected テーブルの内容が $actual テーブルの内容と一致しないときにエラーを報告します。
void assertDataSetsEqual(PHPUnit_Extensions_Database_DataSet_IDataSet $expected, PHPUnit_Extensions_Database_DataSet_IDataSet $actual) $expected データセットの内容が $actual データセットの内容と一致しないときにエラーを報告します。

データセット

Data sets are the basic building blocks for both your database fixture as well as the assertions you may make at the end of your test. When returning a data set as a fixture from the PHPUnit_Extensions_Database_TestCase::getDataSet() method, the default implementation in PHPUnit will automatically truncate all tables specified and then insert the data from your data set in the order specified by the data set. For your convenience there are several different types of data sets that can be used at your convenience.

Flat XML データセット

The flat XML data set is a very simple XML format that uses a single XML element for each row in your data set. An example of a flat XML data set is shown in 例 9.2.

例 9.2: Flat XML データセット

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
  <post
    post_id="1"
    title="My First Post"
    date_created="2008-12-01 12:30:29"
    contents="This is my first post" rating="5"
  />
  <post
    post_id="2"
    title="My Second Post"
    date_created="2008-12-04 15:35:25"
    contents="This is my second post"
  />
  <post
    post_id="3"
    title="My Third Post"
    date_created="2008-12-09 03:17:05"
    contents="This is my third post"
    rating="3"
  />

  <post_comment
    post_comment_id="2"
    post_id="2"
    author="Frank"
    content="That is because this is simply an example."
    url="http://phpun.it/"
  />
  <post_comment
    post_comment_id="1"
    post_id="2"
    author="Tom"
    content="While the topic seemed interesting the content was lacking."
  />

  <current_visitors />
</dataset>


As you can see the formatting is extremely simple. Each of the elements within the root <dataset> element represents a row in the test database with the exception of the last <current_visitors /> element (this will be discussed shortly.) The name of the element is the equivalent of a table name in your database. The name of each attribute is the equivalent of a column name in your databases. The value of each attribute is the equivalent of the value of that column in that row.

This format, while simple, does have some special considerations. The first of these is how you deal with empty tables. With the default operation of CLEAN_INSERT you can specify that you want to truncate a table and not insert any values by listing that table as an element without any attributes. This can be seen in 例 9.2 with the <current_visitors /> element. The most common reason you would want to ensure an empty table as a part of your fixture is when your test should be inserting data to that table. The less data your database has, the quicker your tests will run. So if I where testing my simple blogging software's ability to add comments to a post, I would likely change 例 9.2 to specify post_comment as an empty table. When dealing with assertions it is often useful to ensure that a table is not being unexpectedly written to, which could also be accomplished in FlatXML using the empty table format.

The second consideration is how NULL values are defined. The nature of the flat XML format only allows you to explicitly specify strings for column values. Of course your database will convert a string representation of a number or date into the appropriate data type. However, there are no string representations of NULL. You can imply a NULL value by leaving a column out of your element's attribute list. This will cause a NULL value to be inserted into the database for that column. This leads me right into my next consideration that makes implicit NULL values somewhat difficult to deal with.

The third consideration is how columns are defined. The column list for a given table is determined by the attributes in the first element for any given table. In 例 9.2 the post table would be considered to have the columns post_id, title, date_created, contents and rating. If the first <post> were removed then the post would no longer be considered to have the rating column. This means that the first element of a given name defines the structure of that table. In the simplest of examples, this means that your first defined row must have a value for every column that you expect to have values for in the rest of rows for that table. If an element further into your data set specifies a column that was not specified in the first element then that value will be ignored. You can see how this influenced the order of elements in my dataset in 例 9.2. I had to specify the second <post_comment> element first due to the non-NULL value in the url column.

There is a way to work around the inability to explicitly set NULL in a flat XML data using the Replacement data set type. This will be discussed further in 「データセットの交換」.

XML データセット

While the flat XML data set is simple it also proves to be limiting. A more powerful xml alternative is the XML data set. It is a more structured xml format that allows you to be much more explicit with your data set. An example of the XML data set equivalent to the previous flat XML example is shown in 例 9.3.

例 9.3: XML データセット

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
  <table name="post">
    <column>post_id</column>
    <column>title</column>
    <column>date_created</column>
    <column>contents</column>
    <column>rating</column>
    <row>
      <value>1</value>
      <value>My First Post</value>
      <value>2008-12-01 12:30:29</value>
      <value>This is my first post</value>
      <value>5</value>
    </row>
    <row>
      <value>2</value>
      <value>My Second Post</value>
      <value>2008-12-04 15:35:25</value>
      <value>This is my second post</value>
      <null />
    </row>
    <row>
      <value>3</value>
      <value>My Third Post</value>
      <value>2008-12-09 03:17:05</value>
      <value>This is my third post</value>
      <value>3</value>
    </row>
  </table>
  <table name="post_comment">
    <column>post_comment_id</column>
    <column>post_id</column>
    <column>author</column>
    <column>content</column>
    <column>url</column>
    <row>
      <value>1</value>
      <value>2</value>
      <value>Tom</value>
      <value>While the topic seemed interesting the content was lacking.</value>
      <null />
    </row>
    <row>
      <value>2</value>
      <value>2</value>
      <value>Frank</value>
      <value>That is because this is simply an example.</value>
      <value>http://phpun.it</value>
    </row>
  </table>
  <table name="current_visitors">
    <column>current_visitors_id</column>
    <column>ip</column>
  </table>
</dataset>


The formatting is more verbose than that of the Flat XML data set but in some ways much easier to understand. The root element is again the <dataset> element. Then directly under that element you will have one or more <table> elements. Each <table> element must have a name attribute with a value equivalent to the name of the table being represented. The <table> element will then include one or more <column> elements containing the name of a column in that table. The <table> element will also include any number (including zero) of <row> elements. The <row> element will be what ultimately specifies the data to store/remove/update in the database or to check the current database against. The <row> element must have the same number of children as there are <column> elements in that <table> element. The order of your child elements is also determined by the order of your <column> elements for that table. There are two different elements that can be used as children of the <row> element. You may use the <value> element to specify the value of that column in much the same way you can use attributes in the flat XML data set. The content of the <value> element will be considered the content of that column for that row. You may also use the <null> element to explicitly indicate that the column is to be given a NULL value. The <null> element does not contain any attributes or children.

You can use the DTD in 例 9.4 to validate your XML data set files. A reference of the valid elements in an XML data set can be found in 表 9.2

例 9.4: The XML Data Set DTD

<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT dataset (table+) | ANY>
<!ELEMENT table (column*, row*)>
<!ATTLIST table
    name CDATA #REQUIRED
>
<!ELEMENT column (#PCDATA)>
<!ELEMENT row (value | null | none)*>
<!ELEMENT value (#PCDATA)>
<!ELEMENT null EMPTY>


表9.2 XML Data Set Element Description

Element Purpose Contents Attributes
<dataset> The root element of the xml data set file. One or more <table> elements. None
<table> Defines a table in the dataset. One or more <column> elements and zero or more <row> elements. name - The name of the table.
<column> Defines a column in the current table. A text node containing the name of the column. None
<row> Defines a row in the table. One or more <value> or <null> elements. None
<value> Sets the value of the column in the same position as this value. A text node containing the value of the corresponding column. None
<null> Sets the value of the column in the same position of this value to NULL. None None

CSV Data Set

While XML data sets provide a convenient way to structure your data, in many cases they can be time consuming to hand edit as there are not very many XML editors that provide an easy way to edit tabular data via xml. In these cases you may find the CSV data set to be much more useful. The CSV data set is very simple to understand. Each CSV file represents a table. The first row in the CSV file must contain the column names and all subsequent rows will contain the data for those columns.

To construct a CSV data set you must instantiate the PHPUnit_Extensions_Database_DataSet_CsvDataSet class. The constructor takes three parameters: $delimiter, $enclosure and $escape. These parameters allow you to specify the exact formatting of rows within your CSV file. So this of course means it doesn't have to really be a CSV file. You can also provide a tab delimited file. The default constructor will specify a comma delimited file with fields enclosed by a double quote. If there is a double quote within a value it will be escaped by an additional double quote. This is as close to an accepted standard for CSV as there is.

Once your CSV data set class is instantiated you can use the method addTable() to add CSV files as tables to your data set. The addTable method takes two parameters. The first is $tableName and contains the name of the table you are adding. The second is $csvFile and contains the path to the CSV you will be using to set the data for that table. You can call addTable() for each table you would like to add to your data set.

In 例 9.5 you can see an example of how three CSV files can be combined into the database fixture for a database test case.

例 9.5: CSV Data Set Example

 
--- fixture/post.csv ---
post_id,title,date_created,contents,rating
1,My First Post,2008-12-01 12:30:29,This is my first post,5
2,My Second Post,2008-12-04 15:35:25,This is my second post,
3,My Third Post,2008-12-09 03:17:05,This is my third post,3

--- fixture/post_comment.csv ---
post_comment_id,post_id,author,content,url
1,2,Tom,While the topic seemed interesting the content was lacking.,
2,2,Frank,That is because this is simply an example.,http://phpun.it

--- fixture/current_visitors.csv ---
current_visitors_id,ip

--- DatabaseTest.php ---
<?php
require_once 'PHPUnit/Extensions/Database/TestCase.php';
require_once 'PHPUnit/Extensions/Database/DataSet/CsvDataSet.php';

class DatabaseTest extends PHPUnit_Extensions_Database_TestCase
{
protected function getConnection()
{
$pdo = new PDO('mysql:host=localhost;dbname=testdb', 'root', '');
return $this->createDefaultDBConnection($pdo, 'testdb');
}

protected function getDataSet()
{
$dataSet = new PHPUnit_Extensions_Database_DataSet_CsvDataSet();
$dataSet->addTable('post', 'fixture/post.csv');
$dataSet->addTable('post_comment', 'fixture/post_comment.csv');
$dataSet->addTable('current_visitors', 'fixture/current_visitors.csv');
return $dataSet;
}
}
?>

Unfortunately, while the CSV dataset is appealing from the aspect of edit-ability, it has the same problems with NULL values as the flat XML dataset. There is no native way to explicitly specify a null value. If you do not specify a value (as I have done with some of the fields above) then the value will actually be set to that data type's equivalent of an empty string. Which in most cases will not be what you want. I will address this shortcoming of both the CSV and the flat XML data sets shortly.

データセットの交換

...

操作

...

データベースのテストのコツ

...

Prev Next
1. 自動テスト
2. PHPUnit の目標
3. PHPUnit のインストール
4. PHPUnit 用のテストの書き方
テストの依存性
データプロバイダ
例外のテスト
PHP のエラーのテスト
5. コマンドラインのテストランナー
6. Fixtures
tearDown() よりも setUp()
バリエーション
Fixture の共有
グローバルな状態
7. テストの構成
ファイルシステムを用いたテストスイートの構成
XML 設定ファイルを用いたテストスイートの構成
テストケースクラスの使用法
8. テストケースの拡張
出力内容のテスト
9. データベースのテスト
データセット
Flat XML データセット
XML データセット
CSV Data Set
データセットの交換
操作
データベースのテストのコツ
10. 不完全なテスト・テストの省略
不完全なテスト
テストの省略
11. テストダブル
スタブ
モックオブジェクト
ウェブサービスのスタブおよびモック
ファイルシステムのモック
12. テストの進め方
開発中のテスト
デバッグ中のテスト
13. テスト駆動開発
銀行口座の例
14. 振舞駆動開発
ボウリングゲームの例
15. コードカバレッジ解析
カバーするメソッドの指定
コードブロックの無視
ファイルのインクルードや除外
16. テストのその他の使用法
アジャイルな文書作成
複数チームでのテスト
17. 雛形ジェネレータ
テストケースクラスの雛形の作成
テストケースクラスからのクラスの雛形の作成
18. PHPUnit と Selenium
Selenium RC
PHPUnit_Extensions_SeleniumTestCase
19. ログ出力
テスト結果 (XML)
テスト結果 (TAP)
テスト結果 (JSON)
コードカバレッジ (XML)
20. ビルドの自動化
Apache Ant
Apache Maven
Phing
21. 継続的インテグレーション
Atlassian Bamboo
CruiseControl
phpUnderControl
22. PHPUnit API
概要
PHPUnit_Framework_Assert
assertArrayHasKey()
assertClassHasAttribute()
assertClassHasStaticAttribute()
assertContains()
assertContainsOnly()
assertEqualXMLStructure()
assertEquals()
assertFalse()
assertFileEquals()
assertFileExists()
assertGreaterThan()
assertGreaterThanOrEqual()
assertLessThan()
assertLessThanOrEqual()
assertNull()
assertObjectHasAttribute()
assertRegExp()
assertSame()
assertSelectCount()
assertSelectEquals()
assertSelectRegExp()
assertStringEndsWith()
assertStringEqualsFile()
assertStringStartsWith()
assertTag()
assertThat()
assertTrue()
assertType()
assertXmlFileEqualsXmlFile()
assertXmlStringEqualsXmlFile()
assertXmlStringEqualsXmlString()
PHPUnit_Framework_Test
PHPUnit_Framework_TestCase
PHPUnit_Framework_TestSuite
PHPUnit_Framework_TestResult
パッケージの構成
23. PHPUnit の拡張
PHPUnit_Framework_TestCase のサブクラスの作成
アサートクラスの作成
PHPUnit_Extensions_TestDecorator のサブクラスの作成
PHPUnit_Framework_Test の実装
PHPUnit_Framework_TestResult のサブクラスの作成
PHPUnit_Framework_TestListener の実装
新しいテストランナーの作成
A. アサーション
B. アノテーション
@assert
@backupGlobals
@backupStaticAttributes
@covers
@dataProvider
@depends
@expectedException
@group
@outputBuffering
@runTestsInSeparateProcesses
@runInSeparateProcess
@test
@testdox
@ticket
C. XML 設定ファイル
PHPUnit
テストスイート
グループ
コードカバレッジ対象のファイルの追加や除外
ログ出力
テストリスナー
PHP INI 項目や定数、グローバル変数の設定
Selenium RC の設定ブラウザ
D. 目次
E. 参考文献
F. 著作権