PHPUnit / DbUnitの操作の実践に関する多くのテキスト

こんにちは、友達!

MySQLと連携してPHPUnit / DbUnitを処理した経験を共有したいと思います。 さらに、小さな背景。



簡単な背景



単一のWebアプリケーションを作成する過程で、MySQLデータベースと集中的にやり取りするPHPコードをテストする必要が生じました。 このプロジェクトでは、ユニットテストフレームワークとしてxUnitポートであるPHPUnitを使用しました。 その結果、データベースと直接対話するモジュールのテストを作成し、PHPUnit / DbUnitプラグインを選択することにしました。 次に、テストの作成時に発生した問題と、それらをどのように克服したかについて説明します。 それに応じて、私の決定の正確性に関する知識のある人々のコメントを受け取りたいと思います。



DbUnitの仕組み



この副次句は、PHPUnitおよび/またはDbUnitを使用したテスト方法に精通していない人を対象としています。 興味のない人は誰でも安全にへ進むことができます。



さらに本文:

サブアイテムは初心者向けであるため、最初に通常のPHPクラスのユニットテストの手順を検討し、データベースと対話するコードをテストする際の違いを説明します。



通常のPHPクラスのテスト


PHPUnitフレームワークを使用してPHPで記述されたクラスをテストするには、基本クラスPHPUnit_Framework_TestCaseを拡張するテストクラスを作成する必要があります。 次に、テストという言葉から始めてこのクラスにパブリックメソッドを作成します(異なる方法で呼び出されるメソッドを作成すると、テストの実行時に自動的に呼び出されません)。テストされたクラスのオブジェクトでアクションを実行し、結果をチェックするコードをそれらに配置します。 結果のphpunitクラスを終了してフィードすると、すべてのテストメソッドが順番に呼び出され、それらの作業に関するレポートが親切に提供されます。 ただし、ほとんどの場合、各テストメソッドには、テストオブジェクトを操作するためのシステムを準備するコードが繰り返されます。 コードの重複を避けるため、PHPUnit_Framework_TestCaseクラスでsetUpおよびtearDownプロテクトメソッドが作成されました。これらのクラスには空の実装があります。 これらのメソッドは、それぞれ次のテストメソッドの開始前と開始後に呼び出され、テストアクションの実行のためにシステムを準備し、各テストの完了後にシステムをクリーニングするのに役立ちます。 PHPUnit_Framework_TestCaseを拡張するテストクラスでは、これらのメソッドをオーバーライドし、各テストメソッドでコードを前に繰り返し配置できます。 その結果、テストを実行するときのメソッド呼び出しのシーケンスは次のようになります。

  1.   setUp(){/ *システムを目的の状態に設定* /} 
  2.   testMethod1(){/ *テストされたクラスメソッド1 * /} 
  3.   tearDown(){/ *システムをクリアしました* /} 
  1.   setUp(){/ *システムを目的の状態に設定* /} 
  2.   testMethod2(){/ *テストされたクラスメソッド2 * /} 
  3.   tearDown(){/ *システムをクリアしました* /} 
...

  1.   setUp(){/ *システムを目的の状態に設定* /} 
  2.   testMethodN(){/ *クラスNメソッドをテスト* /} 
  3.   tearDown(){/ *システムをクリアしました* /} 


データベースと対話するPHPコードのテスト


データベースと対話するコードのテストを記述するプロセスは、通常のPHPクラスのテスト手順と実質的に同じです。 最初に、PHPUnit_Extensions_Database_TestCaseを継承するテストクラスを作成する必要があります(PHPUnit_Extensions_Database_TestCaseクラス自体はPHPUnit_Framework_TestCaseを継承します)。テストクラスのメソッドのテストが含まれます。 次に、テストプレフィックスで始まるテストメソッドを作成し、このphpunitコードにテストクラスの名前を入力します。 唯一の違いは、テストクラスで2つのパブリックメソッドgetConnection()とgetDataSet()を実装する必要があることです。 最初の方法は、データベースを操作するようにDbUnitを教えるために必要です( PDOを使用する必要があります)。2番目の方法は、次のテストの前にデータベースの状態をフレームワークに伝えるために必要です。 DbUnitの用語でのDataSetは、1つ以上のテーブルのセットを指します。



前述のように、次のテスト(テストクラスのメソッドで表される)を実行する前に、PHPUnitは特別なsetUp()メソッドを呼び出して、テストされたクラスのオブジェクトのランタイムをエミュレートします。 DbUnitの場合、setUp()メソッドのデフォルトの実装は空ではなくなりました。 一般的に、databaseTesterオブジェクトはsetUp()メソッド内に作成され、私たちが定義したgetConnection()メソッドを使用して、データベースをgetDataSet()メソッドの呼び出し時に取得したテーブルセット(DataSet)で表される状態にします。 注意が必要な場合は、getDataSet()メソッドの実装もテストクラスによって提供される必要があります。 私たちによって。 その結果、同様の呼び出しシーケンスが得られます

  1.   setUp(){/ *受信したデータに従ってデータベースを設定します
                      メソッドgetDataSet()* /} 
  2.   testMethod1(){/ *テストされたクラスメソッド1 * /} 
  3.   tearDown(){/ *システムをクリアしました* /} 
  1.   setUp(){/ *受信したデータに従ってデータベースを設定します
                      メソッドgetDataSet()* /} 
  2.   testMethod2(){/ *テストされたクラスメソッド2 * /} 
  3.   tearDown(){/ *システムをクリアしました* /} 
...

  1.   setUp(){/ *受信したデータに従ってデータベースを設定します
                      メソッドgetDataSet()* /} 
  2.   testMethodN(){/ *クラスNメソッドをテスト* /} 
  3.   tearDown(){/ *システムをクリアしました* /} 


ちょっとしたトラブル



運用環境:プロジェクトで使用されるデータベースには、MySQL InnoDBエンジンという数十のテーブルがあります。 外部キーメカニズムは、データベースレベルでデータの一貫性を維持するために積極的に使用されます。



1.ベースの初期化


テストプロセスに影を落とし始めた最初の迷惑は、作成したテーブルセットでデータベースを初期化することでした。



DbUnitを使用すると、さまざまなソースからデータを受信して​​、DataSetを作成できます。



テーブルセットを作成する上記の各メソッドは、PHPUnit_Extensions_Database_TestCaseクラスの個別のメソッドによって実装されます。



mysqldumpをアシスタントとして選択し、攻撃に突入しました。データベースの必要な状態を形成し、xmlでアンロードし、getDataSet()実装で次のように記述しました。

public function getDataSet() { return $this->createMySQLXMLDataSet('db_init.xml'); // ,  mysqldump. }
      
      







...そして最初のテストを実行することにしました。 しかし、彼はすぐに例外を受け取りました。例外は、外部キーに制限があるためにデータベースを指定された状態にできないと明示的に述べています。



DbUnitソースを数分間掘り下げると、PHPUnit_Extensions_Database_TestCase :: setUp()メソッドで、指定したDataSetに応じた状態にデータベースを設定することが、PHPUnit_Extensions_Database_Operation_Factory :: CLEAN_INSERT操作を使用して実行されることがわかりました。 CLEAN_INSERT操作は、ファクトリによって生成されるマクロであり、PHPUnit_Extensions_Database_Operation_Factory :: TRUNCATEおよびPHPUnit_Extensions_Database_Operation_Factory :: INSERTの2つの操作が含まれます。 明らかに、すべてがここに配置されました-FOREIGN KEY外部キーにアクティブな制限があるデータベースに対してTRUNCATEを作成することはできません。



決める必要があります。 2つの方法があります:テスト中に一時的にFOREIGN KEYを無効にする(ダークパス)か、DbUnitソースコードの喫煙中に見つかった新しいPHPUnit_Extensions_Database_Operation_Factory :: DELETE_ALLコマンドを使用します(ライトが長いパス)。 しばらくして、暗黒面が私を圧倒し、私はより簡単な方法で行くことにしました-接続の作成中に外部キーの整合性制限を無効にします。 幸運にも、作成コードはgetConnection()メソッドの実装で私によってまだ書かれていました。



getConnection()の典型的な実装は次のようになります。

 public function getConnection() { if (is_null($this->m_oConn)) { $oPdo = new PDO('mysql:dbname=db1;host=localhost', 'root', 'qwerty'); $this->m_oConn = $this->createDefaultDBConnection($oPdo, 'db1'); } return $this->m_oConn; }
      
      





$ m_oConnは、PDOのラッパーの一種であるテストクラスのメンバー変数です。 正確には、これはクラスPHPUnit_Extensions_Database_DB_DefaultDatabaseConnectionのインスタンスです。 PDOオブジェクトを作成した直後に行$ oPdo-> exec( 'SET foreign_key_checks = 0')を追加することで、しばらくの間初期化の問題を解決しました。



実際、予想どおり、しばらくしてデータベース内のデータの不整合を伴うレーキに遭遇し、明るいパスに戻らなければなりませんでした。つまり、外部キーの無効化を拒否し、TRUNCATEをDELETE_ALLに置き換えました。



ソースの別のレビューは、PHPUnit_Extensions_Database_TestCase :: setUp()の実装に向けて掘り下げる必要があることを示しました。 彼女のコードは次のとおりです。

 protected function setUp() { parent::setUp(); // PHPUnit_Framework_TestCase::setUp() -   $this->databaseTester = NULL; $this->getDatabaseTester()->setSetUpOperation($this->getSetUpOperation()); $this->getDatabaseTester()->setDataSet($this->getDataSet()); $this->getDatabaseTester()->onSetUp(); }
      
      







ここにgetSetUpOperation()メソッドがあります:

 protected function getSetUpOperation() { return PHPUnit_Extensions_Database_Operation_Factory::CLEAN_INSERT(); }
      
      







テストクラスのgetSetUpOperation()メソッドを次のようにオーバーライドします。

 protected function getSetUpOperation() { return PHPUnit_Extensions_Database_Operation_Factory::INSERT(); }
      
      





TRUNCATEを取り除きましたが、データベースクリーンアップを実装する必要性を自分に追加しました。 データベースには複数のビューが含まれているため、すべてのデータベーステーブルからのDataSetに対するPHPUnit_Extensions_Database_Operation_Factory :: DELETE_ALL()への思いやりのない呼び出しは、何の役にも立ちません。 さらに、データベースのクリーニング機能は、テストの初期化時だけでなく非常に役立つと考えたため、独立した方法として設計することにしました。

 protected function clearDb() { $aTableNames = $this->getConnection()->createDataSet()->getTableNames(); foreach ($aTableNames as $i => $sTableName) { if (false === strpos($sTableName, 'view_')) continue; unset($aTableNames[$i]); } $aTableNames = array_values($aTableNames); $op = \PHPUnit_Extensions_Database_Operation_Factory::DELETE_ALL(); $op->execute($this->getConnection(), $this->getConnection()->createDataSet($aTableNames)); }
      
      





このコードでは、データベースに存在するすべてのビューが接頭辞view_で始まると想定しています。

setUp()メソッドを再定義するだけで、データを入力するためにdatabaseTesterに渡す前にデータベースを自動的にクリーンアップします。

 protected function setUp() { $this->clearDb(); parent::setUp(); }
      
      







2.テーブルセットの比較


次の問題は、2つのDataSetを比較しようとしたときに発生しました-1つはデータベースから直接取得し(テストコードの結果として生成)、もう1つは手作業で事前に作成し、目的の結果を表します。



データベースの現在の状態は、次の方法で取得できます。

 $oActualDataSet = $this->getConnection()->createDataSet();
      
      







PHPUnit_Extensions_Database_TestCase :: assertDataSetsEqualメソッドを見て、mansの2つのテーブルセットを比較して、とてもうれしく思いました。 早すぎることが判明したので。 比較結果は非常に予想外でした。 比較時に2つの同一に見えるテーブルのセットがアサートのクラッシュを引き起こしました。



デバッガーは、データベースから受信したDataSetに問題があることを示しました。 最適化のため、どうやら$ this-> getConnection()-> createDataSet()がテストクラスで呼び出されると、テーブルの部分セットのみがロードされ、正確には、DataSetのメタデータ(データベース名とその他の殻)のみがロードされます。



PHPUnit_Extensions_Database_TestCase :: assertDataSetsEqualのソースコードは次のとおりです。

 public static function assertDataSetsEqual(PHPUnit_Extensions_Database_DataSet_IDataSet $expected, PHPUnit_Extensions_Database_DataSet_IDataSet $actual, $message = '') { $constraint = new PHPUnit_Extensions_Database_Constraint_DataSetIsEqual($expected); self::assertThat($actual, $constraint, $message); }
      
      







呼び出しのチェーンをさらに拡張すると、直接比較操作のいくつかの委任の後、2つのテーブルが比較されるPHPUnit_Extensions_Database_DataSet_AbstractTable :: matches(PHPUnit_Extensions_Database_DataSet_ITable $ other)になります。 この方法では、テーブルを比較するときに、テーブル内のデータは必ずデータベースから取得されます。 しかし、これはこの方法に関して言えばそうです。 2つのDataSetのテーブルを互いに比較する前に、DataSetが比較されるためです。 その結果、ある場所でのアサートは機能しません。 このバグはgithubのPHPUnit / DbUnitの問題にあり、すでに数か月前にあります。



このエラーの修正を見越して、テーブルセットを比較する方法をすばやく提案しました。 DbUnitの精神ではなく、評価するすべての呼び出しのシーケンスによってすべてが行われます->比較されたオブジェクトの特定の実装の一致ですが、動作しています:

 public function compareDataSets(PHPUnit_Extensions_Database_DataSet_IDataSet $expected, PHPUnit_Extensions_Database_DataSet_IDataSet $actual, $message = '') { $aExpectedNames = $expected->getTableNames(); $aActualNames = $actual->getTableNames(); sort($aActualNames); sort($aExpectedNames); $this->assertEquals($aExpectedNames, $aActualNames, $message); foreach ($aActualNames as $sTableName) { $atable = $actual->getTable($sTableName); $etable = $expected->getTable($sTableName); if (0 == $atable->getRowCount()) { $this->assertEquals(0, $etable->getRowCount(), $message); } else { $this->assertTablesEqual($etable, $atable, $message); } } }
      
      







おわりに



この記事で説明されているDbUnitの動作は、DbUnit 1.1.2、PHPUnit 3.6.10、およびMySQL 5.1を使用して取得されました。 上記のすべての松葉杖を追加した結果、PHPUnit_Extensions_Database_TestCaseを拡張し、これらすべてのメソッドを含む基本クラスが作成されました。 ベースで動作するプロジェクトの残りのテストクラスは、このベースクラスから継承されます。



良い人を言い換えると、テストの方法はわかりませんが、とても気に入っています。 それで、私は記事に示された方法についてのコメントを聞きたいです。



All Articles