Zend_Test_PHPUnit_Db(日本語)

データ・アクセスとドメインモデルを組み合わせると、 目標をテストするために、データベースを使う必要がしばしばあります。 しかし、データベースはそれぞれのテスト全体で永続的です。 そして、それは互いに影響を及ぼすことができるテスト結果に至ります。 さらにまた、テストが動作できるようにするためにデータベースを準備することは、 相当な作業です。 PHPUnitデータベース機能拡張では、 それぞれのテストの間でデータベースを準備したり、取り外したりするための、 きわめて単純な手法を提供することにより、 データベースを用いたテストを単純化します。 Zend Frameworkアプリケーションに対する データベース・テストを書くことが単純化されるように、 このコンポーネントは、Zend Frameworkに依存したコードでPHPUnitデータベース機能拡張を拡張します。

データベース・テストは、2つの概念上の実体、DataSets及びDataTablesで説明できます。 内部的には、PHPUnitデータベース機能拡張は、データベース、そのテーブルと、 構成ファイルまたは本当のデータベース内容からなる列を含むオブジェクト構造を構築できます。 そこで、この抽象的なオブジェクト・グラフは、位置指定子を使用して比較できます。 データベース・テストの一般的なユース・ケースは、 種となるデータで一部のテーブルを準備し、 それから操作を一部実行して、 データベース階層で操作されたことが、 あらかじめ定義された期待される、とある状態と等しいことを最終的に示すことです。 Zend_Test_PHPUnit_Dbは、 既存のZend_Db_Table_Abstract またはZend_Db_Table_Rowset_Abstract インスタンスからDataSets及びDataTablesを生成できるようにして、 この作業を単純化します。

さらにまた、このコンポーネントは、どんなZend_Db_Adapter_Abstractでも テストのために統合できるようにします。 ところが、本来の機能拡張は、PDOで機能するだけです。 Zend_Db_Adapter_Abstractのためのテスト・アダプタ実装は、 このコンポーネントにも含まれます。 APIメソッドによって使われる SQLと結果スタックの働きをするDBアダプタを データベースを全く必要としないで、 インスタンス化できるようにします。

Quickstart

Setup a Database TestCase

We are now writting some database tests for the Bug Database example in the Zend_Db_Table documentation. First we begin to test that inserting a new bug is actually saved in the database correctly. First we have to setup a test-class that extends Zend_Test_PHPUnit_DatabaseTestCase. This class extends the PHPUnit Database Extension, which in turn extends the basic PHPUnit_Framework_TestCase. A database testcase contains two abstract methods that have to be implemented, one for the database connection and one for the initial dataset that should be used as seed or fixture.

注記

You should be familiar with the PHPUnit Database extension to follow this quickstart easily. Although all the concepts are explained in this documentation it may be helpful to read the PHPUnit documentation first.

class BugsTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    private $_connectionMock;

    /**
     * Returns the test database connection.
     *
     * @return PHPUnit_Extensions_Database_DB_IDatabaseConnection
     */
    protected function getConnection()
    {
        if($this->_connectionMock == null) {
            $connection = Zend_Db::factory(...);
            $this->_connectionMock = $this->createZendDbConnection(
                $connection, 'zfunittests'
            );
            Zend_Db_Table_Abstract::setDefaultAdapter($connection);
        }
        return $this->_connectionMock;
    }

    /**
     * @return PHPUnit_Extensions_Database_DataSet_IDataSet
     */
    protected function getDataSet()
    {
        return $this->createFlatXmlDataSet(
            dirname(__FILE__) . '/_files/bugsSeed.xml'
        );
    }
}

Here we create the database connection and seed some data into the database. Some important details should be noted on this code:

  • You cannot directly return a Zend_Db_Adapter_Abstract from the getConnection() method, but a PHPUnit specific wrapper which is generated with the createZendDbConnection() method.

  • The database schema (tables and database) is not re-created on every testrun. The database and tables have to be created manually before running the tests.

  • Database tests by default truncate the data during setUp() and then insert the seed data which is returned from the getDataSet() method.

  • DataSets have to implement the interface PHPUnit_Extensions_Database_DataSet_IDataSet. There is a wide range of XML and YAML configuration file types included in PHPUnit which allows to specifiy how the tables and datasets should look like and you should look into the PHPUnit documentation to get the latest information on these dataset specifications.

Specify a seed dataset

In the previous setup for the database testcase we have specified a seed file for the database fixture. We now create this file specified in the Flat XML format:

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
    <zfbugs bug_id="1" bug_description="system needs electricity to run"
        bug_status="NEW" created_on="2007-04-01 00:00:00"
        updated_on="2007-04-01 00:00:00" reported_by="goofy"
        assigned_to="mmouse" verified_by="dduck" />
    <zfbugs bug_id="2" bug_description="Implement Do What I Mean function"
        bug_status="VERIFIED" created_on="2007-04-02 00:00:00"
        updated_on="2007-04-02 00:00:00" reported_by="goofy"
        assigned_to="mmouse" verified_by="dduck" />
    <zfbugs bug_id="3" bug_description="Where are my keys?" bug_status="FIXED"
        created_on="2007-04-03 00:00:00" updated_on="2007-04-03 00:00:00"
        reported_by="dduck" assigned_to="mmouse" verified_by="dduck" />
    <zfbugs bug_id="4" bug_description="Bug no product" bug_status="INCOMPLETE"
        created_on="2007-04-04 00:00:00" updated_on="2007-04-04 00:00:00"
        reported_by="mmouse" assigned_to="goofy" verified_by="dduck" />
</dataset>

We will work with this four entries in the database table "zfbugs" in the next examples. The required MySQL schema for this example is:

CREATE TABLE IF NOT EXISTS `zfbugs` (
    `bug_id` int(11) NOT NULL auto_increment,
    `bug_description` varchar(100) default NULL,
    `bug_status` varchar(20) default NULL,
    `created_on` datetime default NULL,
    `updated_on` datetime default NULL,
    `reported_by` varchar(100) default NULL,
    `assigned_to` varchar(100) default NULL,
    `verified_by` varchar(100) default NULL,
PRIMARY KEY  (`bug_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 ;

A few initial database tests

Now that we have implemented the two required abstract methods of the Zend_Test_PHPUnit_DatabaseTestCase and specified the seed database content, which will be re-created for each new test, we can go about to make our first assertion. This will be a test to insert a new bug.

class BugsTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    public function testBugInsertedIntoDatabase()
    {
        $bugsTable = new Bugs();

        $data = array(
            'created_on'      => '2007-03-22 00:00:00',
            'updated_on'      => '2007-03-22 00:00:00',
            'bug_description' => 'Something wrong',
            'bug_status'      => 'NEW',
            'reported_by'     => 'garfield',
            'verified_by'     => 'garfield',
            'assigned_to'     => 'mmouse',
        );

        $bugsTable->insert($data);

        $ds = new Zend_Test_PHPUnit_Db_DataSet_QueryDataSet(
            $this->getConnection()
        );
        $ds->addTable('zfbugs', 'SELECT * FROM zfbugs');

        $this->assertDataSetsEqual(
            $this->createFlatXmlDataSet(dirname(__FILE__)
                                      . "/_files/bugsInsertIntoAssertion.xml"),
            $ds
        );
    }
}

Now up to the $bugsTable->insert($data); everything looks familiar. The lines after that contain the assertion methodname. We want to verify that after inserting the new bug the database has been updated correctly with the given data. For this we create a Zend_Test_PHPUnit_Db_DataSet_QueryDataSet instance and give it a database connection. We will then tell this dataset that it contains a table "zfbugs" which is given by an SQL statement. This current/actual state of the database is compared to the expected database state which is contained in another XML file "bugsInsertIntoAssertions.xml". This XML file is a slight deviation from the one given above and contains another row with the expected data:

<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
    <!-- previous 4 rows -->
    <zfbugs bug_id="5" bug_description="Something wrong" bug_status="NEW"
        created_on="2007-03-22 00:00:00" updated_on="2007-03-22 00:00:00"
        reported_by="garfield" assigned_to="mmouse" verified_by="garfield" />
</dataset>

There are other ways to assert that the current database state equals an expected state. The "Bugs" table in the example already knows a lot about its inner state, so why not use this to our advantage? The next example will assert that deleting from the database is possible:

class BugsTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    public function testBugDelete()
    {
        $bugsTable = new Bugs();

        $bugsTable->delete(
            $bugsTable->getAdapter()->quoteInto("bug_id = ?", 4)
        );

        $ds = new Zend_Test_PHPUnit_Db_DataSet_DbTableDataSet();
        $ds->addTable($bugsTable);

        $this->assertDataSetsEqual(
            $this->createFlatXmlDataSet(dirname(__FILE__)
                                      . "/_files/bugsDeleteAssertion.xml"),
            $ds
        );
    }
}

We have created a Zend_Test_PHPUnit_Db_DataSet_DbTableDataSet dataset here, which takes any Zend_Db_Table_Abstract instance and adds it to the dataset with its table name, in this example "zfbugs". You could add several tables more if you wanted using the method addTable() if you want to check for expected database state in more than one table.

Here we only have one table and check against an expected database state in "bugsDeleteAssertion.xml" which is the original seed dataset without the row with id 4.

Since we have only checked that two specific tables (not datasets) are equal in the previous examples we should also look at how to assert that two tables are equal. Therefore we will add another test to our TestCase which verifies updating behaviour of a dataset.

class BugsTest extends Zend_Test_PHPUnit_DatabaseTestCase
{
    public function testBugUpdate()
    {
        $bugsTable = new Bugs();

        $data = array(
            'updated_on'      => '2007-05-23',
            'bug_status'      => 'FIXED'
        );

        $where = $bugsTable->getAdapter()->quoteInto('bug_id = ?', 1);

        $bugsTable->update($data, $where);

        $rowset = $bugsTable->fetchAll();

        $ds        = new Zend_Test_PHPUnit_Db_DataSet_DbRowset($rowset);
        $assertion = $this->createFlatXmlDataSet(
            dirname(__FILE__) . '/_files/bugsUpdateAssertion.xml'
        );
        $expectedRowsets = $assertion->getTable('zfbugs');

        $this->assertTablesEqual(
            $expectedRowsets, $ds
        );
    }
}

Here we create the current database state from a Zend_Db_Table_Rowset_Abstract instance in conjunction with the Zend_Test_PHPUnit_Db_DataSet_DbRowset($rowset) instance which creates an internal data-representation of the rowset. This can again be compared against another data-table by using the $this->assertTablesEqual() assertion.

Usage, API and Extensions Points

The Quickstart already gave a good introduction on how database testing can be done using PHPUnit and the Zend Framework. This section gives an overview over the API that the Zend_Test_PHPUnit_Db component comes with and how it works internally.

Some Remarks on Database Testing

Just as the Controller TestCase is testing an application at an integration level, the Database TestCase is an integration testing method. Its using several different application layers for testing purposes and therefore should be consumed with caution.

It should be noted that testing domain and business logic with integration tests such as Zend Framework's Controller and Database TestCases is a bad practice. The purpose of an Integration test is to check that several parts of an application work smoothly when wired together. These integration tests do not replace the need for a set of unit tests that test the domain and business logic at a much smaller level, the isolated class.

The Zend_Test_PHPUnit_DatabaseTestCase class

The Zend_Test_PHPUnit_DatabaseTestCase class derives from the PHPUnit_Extensions_Database_TestCase which allows to setup tests with a fresh database fixture on each run easily. The Zend implementation offers some additional convenience features over the PHPUnit Database extension when it comes to using Zend_Db resources inside your tests. The workflow of a database test-case can be described as follows.

  1. For each test PHPUnit creates a new instance of the TestCase and calls the setUp() method.

  2. The Database TestCase creates an instance of a Database Tester which handles the setting up and tearing down of the database.

  3. The database tester collects the information on the database connection and initial dataset from getConnection() and getDataSet() which are both abstract methods and have to be implemented by any Database Testcase.

  4. By default the database tester truncates the tables specified in the given dataset, and then inserts the data given as initial fixture.

  5. When the database tester has finished setting up the database, PHPUnit runs the test.

  6. After running the test, tearDown() is called. Because the database is wiped in setUp() before inserting the required initial fixture, no actions are executed by the database tester at this stage.

注記

The Database TestCase expects the database schema and tables to be setup correctly to run the tests. There is no mechanism to create and tear down database tables.

The Zend_Test_PHPUnit_DatabaseTestCase class has some convenience functions that can help writing tests that interact with the database and the database testing extension.

The next table lists only the new methods compared to the PHPUnit_Extensions_Database_TestCase, whose API is documented in the PHPUnit Documentation.

表164 Zend_Test_PHPUnit_DatabaseTestCase API Methods

Method Description
createZendDbConnection(Zend_Db_Adapter_Abstract $connection, $schema) Create a PHPUnit Database Extension compatible Connection instance from a Zend_Db_Adapter_Abstract instance. This method should be used in for testcase setup when implementing the abstract getConnection() method of the database testcase.
getAdapter() Convenience method to access the underlying Zend_Db_Adapter_Abstract instance which is nested inside the PHPUnit database connection created with getConnection().
createDbRowset(Zend_Db_Table_Rowset_Abstract $rowset, $tableName = null) Create a DataTable Object that is filled with the data from a given Zend_Db_Table_Rowset_Abstract instance. The table the rowset is connected to is chosen when $tableName is NULL.
createDbTable(Zend_Db_Table_Abstract $table, $where = null, $order = null, $count = null, $offset = null) Create a DataTable object that represents the data contained in a Zend_Db_Table_Abstract instance. For retrieving the data fetchAll() is used, where the optional parameters can be used to restrict the data table to a certain subset.
createDbTableDataSet(array $tables=array()) Create a DataSet containing the given $tables, an array of Zend_Db_Table_Abstract instances.

Integrating Database Testing with the ControllerTestCase

Because PHP does not support multiple inheritance it is not possible to use the Controller and Database testcases in conjunction. However you can use the Zend_Test_PHPUnit_Db_SimpleTester database tester in your controller test-case to setup a database enviroment fixture for each new controller test. The Database TestCase in general is only a set of convenience functions which can also be accessed and used without the test case.

例908 Database integration example

This example extends the User Controller Test from the Zend_Test_PHPUnit_ControllerTestCase documentation to include a database setup.

class UserControllerTest extends Zend_Test_PHPUnit_ControllerTestCase
{
    public function setUp()
    {
        $this->setupDatabase();
        $this->bootstrap = array($this, 'appBootstrap');
        parent::setUp();
    }

    public function setupDatabase()
    {
        $db = Zend_Db::factory(...);
        $connection = new Zend_Test_PHPUnit_Db_Connection($db,
                                                      'database_schema_name');
        $databaseTester = new Zend_Test_PHPUnit_Db_SimpleTester($connection);

        $databaseFixture =
                    new PHPUnit_Extensions_Database_DataSet_FlatXmlDataSet(
                        dirname(__FILE__) . '/_files/initialUserFixture.xml'
                    );

        $databaseTester->setupDatabase($databaseFixture);
    }
}

Now the Flat XML dataset "initialUserFixture.xml" is used to set the database into an initial state before each test, exactly as the DatabaseTestCase works internally.


データベース・テスト・アダプタの使用

アプリケーションの一部を本当のデータベースでテストしたくなくても、 結合度のせいでせざるを得ないときもあります。 Zend_Test_DbAdapter では、 データベース接続を開始する必要なしに、 Zend_Db_Adapter_Abstract の実装を使う便利な方法を提供します。 さらに、それはコンストラクタ引数を必要としないので、 このアダプタではPHPUnitテストスイート内から非常に簡単にモックアップを作れます。

テスト・アダプタは、いろいろなデータベース結果のためのスタックとして動作します。 結果のその順序は実装したユーザー側の担当でなければいけません。 そして、それは多くの異なるデータベース・クエリを呼ぶテストのための退屈な仕事であるかもしれません。 しかし、少数のクエリだけが実行されるヘルパーが、テストにはまさに適切なヘルパーです。 あなたはユーザー担当のコードに返されなければならない結果の正確な順序を知っています。

$adapter   = new Zend_Test_DbAdapter();
$stmt1Rows = array(array('foo' => 'bar'), array('foo' => 'baz'));
$stmt1     = Zend_Test_DbStatement::createSelectStatement($stmt1Rows);
$adapter->appendStatementToStack($stmt1);

$stmt2Rows = array(array('foo' => 'bar'), array('foo' => 'baz'));
$stmt2     = Zend_Test_DbStatement::createSelectStatement($stmt2Rows);
$adapter->appendStatementToStack($stmt2);

$rs = $adapter->query('SELECT ...'); // Returns Statement 2
while ($row = $rs->fetch()) {
    echo $rs['foo']; // Prints "Bar", "Baz"
}
$rs = $adapter->query('SELECT ...'); // Returns Statement 1

本当のデータベース・アダプタいずれの振る舞いでも、 できる限り fetchAll()fetchObject() 、 及び fetchColumn などのように、 そのようなメソッドができる限りテスト・アダプタとして動作するように シミュレーションされます。

結果スタックにINSERT、UPDATE及びDELETE命令を入れることもできます。 しかしながらそれらは、 $stmt->rowCount() の結果を指定できる命令だけを返します。

$adapter = new Zend_Test_DbAdapter();
$adapter->appendStatementToStack(
    Zend_Test_DbStatement::createInsertStatement(1)
);
$adapter->appendStatementToStack(
    Zend_Test_DbStatement::createUpdateStatement(2)
);
$adapter->appendStatementToStack(
    Zend_Test_DbStatement::createDeleteStatement(10
));

クエリ・プロファイラはデフォルトで有効で、 正しく実行されたか検査するために、 実行した SQL 文とそのバウンドされたパラメータを取得できます。

$adapter = new Zend_Test_DbAdapter();
$stmt = $adapter->query("SELECT * FROM bugs");

$qp = $adapter->getProfiler()->getLastQueryProfile();

echo $qp->getQuerY(); // SELECT * FROM bugs

テスト・アダプタは、指定されたクエリが本当に、 スタックから次に返されるSELECT、DELETE、INSERTまたはUPDATEタイプかどうかは 決して調べません。 データを返す正しい順位は、テスト・アダプタのユーザーによって実装されなければなりません。

テスト・アダプタでは、 listTables()describeTables() 及び lastInsertId() メソッドの使用をシミュレーションするための メソッドの指定も行います。 さらに、 setQuoteIdentifierSymbol() を使用して、 引用符で囲むために使用するべき記号を指定できます。既定では何も使用されません。