バージョン選択

フォーラム

メニュー

オンライン状況

84 人のユーザが現在オンラインです。 (73 人のユーザが フォーラム を参照しています。)
登録ユーザ: 0
ゲスト: 84
もっと...

サイト内検索

バグ報告 > その他 > PHPUnitによるテストが失敗する

その他

新規スレッドを追加する

スレッド表示 | 新しいものから 前のトピック | 次のトピック | 下へ
投稿者 スレッド
snitta
投稿日時: 2014/1/31 12:25
対応状況: −−−
一人前
登録日: 2013/10/3
居住地: 島根県
投稿: 100
PHPUnitによるテストが失敗する
------------------------------------------------------------------------------------------
[EC-CUBE] 2.13.0-dev (http://svn.ec-cube.net/open_trac/browser/branches/version-2_13-dev?rev=23329)
[OS] CentOS 6.5
[PHP] PHP 5.3
[データベース] MySQL 5.6
[WEBサーバ] Apache 2.2.15
[ブラウザ] Firefox 26.0
[現象] PHPUnitによるテストが失敗する
------------------------------------------------------------------------------------------

いつもお世話になっております。
PHPUnitによるテストのうち Common_TestCase::$objQuery を使っているテストについて、
実行環境によっては失敗する場合があります。

発生条件の一例:
* DB_TYPE が mysql である事。
* コマンドラインにおいて mysql_connect() 後の character_set_client が utf8 以外である事。

再現手順:
$ ./eccube_install.sh mysql
(省略)
$ php -r 'error_reporting(E_ALL & ~E_DEPRECATED); $c = mysql_connect("localhost", "user", "password"); echo mysql_client_encoding($c);'
latin1
$ phpunit tests
PHPUnit 3.7.29 by Sebastian Bergmann.

...............................................................  63 / 502 ( 12%)
............................F......FFFF........................ 126 / 502 ( 25%)
............................................................... 189 / 502 ( 37%)
............................................................... 252 / 502 ( 50%)
............................................................... 315 / 502 ( 62%)
............................................................... 378 / 502 ( 75%)
.....................Hello, World!!                                                                                                                                                                                                                                                                
.Hello, World!!Hello.....FFFF................................ 441 / 502 ( 87%)
.............................................................

Time: 6.09 seconds, Memory: 37.75Mb

There were 9 failures:

1) SC_Product_getDetailAndProductsClassTest::testGetDetailAndProductsClass_商品規格IDの商品情報と規格情報を返す
商品詳細+規格
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
     'rank1' => null
-    'class_name1' => '味'
+    'class_name1' => '?'
     'class_id1' => '1'
     'classcategory_id1' => '1001'
     'classcategory_id2' => '1002'
     'classcategory_name2' => 'cat1002'
     'rank2' => null
-    'class_name2' => '味'
+    'class_name2' => '?'
     'class_id2' => '1'
     'price01_inctax' => 1575.0
     'price02_inctax' => 1575.0
 )

/var/www/html/eccube-2.13-dev/tests/class/Common_TestCase.php:53
/var/www/html/eccube-2.13-dev/tests/class/SC_Product/SC_Product_getDetailAndProductsClassTest.php:123

(以下省略)



テストが失敗する原因:
PHPUnitのグローバル変数バックアップのせい?

失敗したテストケースではDBから取得したデータが文字化けしていました。
そこからMySQLのクエリーログを調べると MDB2::setCharset() が適用されていない接続が使用されていました。
次にこの辺りの処理を順に追っていくと以下のようになります。
https://github.com/sebastianbergmann/phpunit/blob/3.7.29/PHPUnit/Framework/TestCase.php#L802-L810

1. テスト前にグローバル変数を serialize() で保存。
2. テスト実行。
3. テスト後にグローバル変数を unserialize() で復元。

最初のテストの 3. でオリジナルのMDB2オブジェクトがGC対象になりデータベース接続が切れます。
次のテストでは復元されたMDB2オブジェクトによって自動的に再接続されますが、
MDB2::setCharset() が呼ばれないために文字化けに繋がっているのでは?と考えます。


対処方法:
ならグローバル変数の復元対象からMDB2を外そうという安直な思考で
PHPUnit_Framework_TestCase::$backupGlobalsBlacklist を指定すると解消しました。

Index: tests/class/Common_TestCase.php
===================================================================
--- tests/class/Common_TestCase.php	(revision 23329)
+++ tests/class/Common_TestCase.php	(working copy)
@@ -16,6 +16,15 @@
  */
 class Common_TestCase extends PHPUnit_Framework_TestCase
 {
+  /**
+   * @var array
+   * @see PHPUnit_Framework_TestCase::$backupGlobals
+   * @see PHPUnit_Framework_TestCase::$backupGlobalsBlacklist
+   */
+  protected $backupGlobalsBlacklist = array(
+      '_MDB2_databases',
+      '_MDB2_dsninfo_default',
+  );
 
   /** SC_Query インスタンス */
   protected $objQuery;


原因や対処はこれで本当によいのか、どなたかご検証頂けましたら幸いです。
よろしくお願いいたします。


----------------
Seiji Nitta
zenith6@gmail.com
https://github.com/zenith6/

nanasess
投稿日時: 2014/1/31 13:22
対応状況: −−−
登録日: 2006/9/9
居住地:
投稿: 2303
Re: PHPUnitによるテストが失敗する
どうしても character_set_client を変更できない環境は別として、通常 PHPUnit を実行する環境(ローカル環境や CIサーバーなど)は設定できると思いますので、my.cnf などで、character_set_client を正しく設定するのが良いのではないでしょうか。

どうしても character_set_client を変更できないのでしたら、 $backupGlobalsBlacklist をオーバーライドしてあげるのは良いと思います。

テストケースの観点からすると、テストとは直接関係のない設定を、テストプログラムにコーディングするのは、あまり良くありません。
snitta
投稿日時: 2014/1/31 14:13
対応状況: −−−
一人前
登録日: 2013/10/3
居住地: 島根県
投稿: 100
Re: PHPUnitによるテストが失敗する
nanasess 様
早速のご回答ありがとうございます!

引用:
どうしても character_set_client を変更できない環境は別として、通常 PHPUnit を実行する環境(ローカル環境や CIサーバーなど)は設定できると思いますので、my.cnf などで、character_set_client を正しく設定するのが良いのではないでしょうか。


テスト環境の不備という点でしたらまさにおっしゃられる通りですが、今回の問題はエンコーディングだけではなく、テスト毎にテスト条件が変わってしまうという所にあると思います。

具体的には SC_Query のコンストラクタで MDB2::setCharset() を呼んでいますが、MySQL/PostgreSQLの実装では mysql_set_charset() や pg_set_client_encoding() を実行するだけで再接続の考慮はされていません。
同じ Common_TestCase::setUp() を通しても初回のテストではエンコーディングが指定してある、二回目以降のテストでは指定されていない、というのは問題ではないでしょうか…。

参考にした個所はこちらです。
http://svn.ec-cube.net/open_trac/browser/branches/version-2_13-dev/data/class/SC_Query.php?rev=23329#L82
http://svn.ec-cube.net/open_trac/browser/branches/version-2_13-dev/data/module/MDB2/Driver/mysql.php?rev=23329#L626
http://svn.ec-cube.net/open_trac/browser/branches/version-2_13-dev/data/module/MDB2/Driver/pgsql.php?rev=23329#L538

引用:
テストケースの観点からすると、テストとは直接関係のない設定を、テストプログラムにコーディングするのは、あまり良くありません。

勉強になります。確かにあまり気持ちのいいやり方ではないですね。
これはPHPUnitとMDB2の問題ですので phpunit.xml 辺りに追い出す方法がないか探しましたが見つけられませんでした。
# グローバル変数バックアップ機能の切り替えだけはありました。
http://phpunit.de/manual/3.7/ja/appendixes.annotations.html#appendixes.annotations.backupGlobals


----------------
Seiji Nitta
zenith6@gmail.com
https://github.com/zenith6/

nanasess
投稿日時: 2014/1/31 16:29
対応状況: −−−
登録日: 2006/9/9
居住地:
投稿: 2303
Re: PHPUnitによるテストが失敗する
引用:

テスト環境の不備という点でしたらまさにおっしゃられる通りですが、今回の問題はエンコーディングだけではなく、テスト毎にテスト条件が変わってしまうという所にあると思います。


1回目と2回目の条件が変化してしまうのは問題ですね。。

SC_Query で MDB2::setCharset() をコールしているのは、character_set_client = latin1 などの環境を考慮して、「念のために」再設定(イレギュラー対応)しているものなので、個人的には、

1. 条件が変化しないよう、サーバー側の環境面で差異を無くした上で
2. 本来の関心事である、アプリケーション側のユニットテストを実行。
3. 別途 SC_Query のユニットテスト内で、 MDB2::setCharset() のテスト(イレギュラーケースのテスト)

という流れが良いのではないかと思っています。

どうしても、アプリケーション側のテストで MDB2::setCharset() のテストをしたい場合は、 $backupGlobalsBlacklist をテストケースでオーバーライドするしかなさそうですね。。
snitta
投稿日時: 2014/1/31 19:27
対応状況: −−−
一人前
登録日: 2013/10/3
居住地: 島根県
投稿: 100
Re: PHPUnitによるテストが失敗する
ご返事ありがとうございます。
エンコーディング指定はおまけなのですね。参考になります。
ただ他にも SC_DB_DBFactory::initObjQuery() 等で設定初期化用のクエリーが発行されており、
これらを開発者全員が把握してテスト環境をすり合わせるのは大変だと思います。
http://svn.ec-cube.net/open_trac/browser/branches/version-2_13-dev/data/class/db/dbfactory/SC_DB_DBFactory_MYSQL.php?rev=23329#L361

とここまで言っておいてふと思いつきました。
PHPUnitによって意図しない再接続が起きるのが問題なのだから、
自分で明示的に新規接続してしまえば良いのではと。

Index: tests/class/Common_TestCase.php
===================================================================
--- tests/class/Common_TestCase.php	(revision 23329)
+++ tests/class/Common_TestCase.php	(working copy)
@@ -27,7 +27,7 @@
 
   protected function setUp()
   {
-    $this->objQuery = SC_Query_Ex::getSingletonInstance('', true);
+    $this->objQuery = SC_Query_Ex::getSingletonInstance('', true, true);
     $this->objQuery->begin();
   }


これならテスト毎に初期化コードも走り、PHPUnitによってMDB2がおかしくなろうと関係ないはず…と思いましたが結果は同じでした。
SC_Query_Ex::getSingletonInstance($dsn = '', $force_run = false, $new = false) の $new は新規接続するかどうかですよね。
なぜでしょう…w

ちょっと時間が足りず中途半端ですが、ここまでのご報告とさせて下さい。
また時間が取れ次第調べたいと思います。
nanasess 様お付き合いありがとうございました。


----------------
Seiji Nitta
zenith6@gmail.com
https://github.com/zenith6/

snitta
投稿日時: 2014/2/2 22:03
対応状況: −−−
一人前
登録日: 2013/10/3
居住地: 島根県
投稿: 100
Re: PHPUnitによるテストが失敗する
PHPUnitによるMDB2の意図しない切断について、
やはり環境で問題をカバーするのではなくテスト自体を修正できないかと思います。
そこで改善方法を二つ程考えましたので、よろしければお目通し頂きたいです。

1. MDB2のインスタンスをバックアップの対象にしない。
2. 各テスト毎にMDB2のインスタンスを新規作成&破棄する。


1. の解決方法は前回同様、PHPUnit_Framework_TestCase::$backupGlobalsBlacklist を指定するものです。
DB接続をテスト間で共有するという前提が必要ですが、
元々の Common_TestCase::setUp() がコネクションプールから取っており、
またテスト毎にプールをクリアしていないからいいかな?と思ってます。

Index: tests/class/Common_TestCase.php
===================================================================
--- tests/class/Common_TestCase.php	(revision 23329)
+++ tests/class/Common_TestCase.php	(working copy)
@@ -16,6 +16,15 @@
  */
 class Common_TestCase extends PHPUnit_Framework_TestCase
 {
+  /**
+   * @var array
+   * @see PHPUnit_Framework_TestCase::$backupGlobals
+   * @see PHPUnit_Framework_TestCase::$backupGlobalsBlacklist
+   */
+  protected $backupGlobalsBlacklist = array(
+      '_MDB2_databases',
+      '_MDB2_dsninfo_default',
+  );
 
   /** SC_Query インスタンス */
   protected $objQuery;


参考: http://phpunit.de/manual/3.7/ja/fixtures.html
引用:
クラスの静的属性のバックアップ・リストアの実装には、PHP 5.3 (あるいはそれ以降のバージョン) が必要です。
グローバル変数やクラスの静的属性のバックアップ・リストアの実装には serialize() および unserialize() を使用しています。
PHP 組み込みの一部のクラス、たとえば PDO のオブジェクトはシリアライズできないため、
そのようなオブジェクトが $GLOBALS 配列に格納されている場合はバックアップ操作が失敗します。



2. の解決方法は不具合で切断されていた挙動を仕様にしてしまおう、というものです。
まずテスト準備段階で html/require.php を読み込んだ時にMDB2オブジェクトが作成されますが、
これをPHPUnitがバックアップしないように開放しておきます。
次に Common_TestCase::setUp() で SC_Query_Ex::getSingletonInstance('', true, true) で新規接続を行います。
これにより各テストで正しい手続きが行われたDB接続を使用できるようになります。

テスト毎に新規接続するパッチ:
Index: data/class/SC_Query.php
===================================================================
--- data/class/SC_Query.php     (リビジョン 23329)
+++ data/class/SC_Query.php     (作業コピー)
@@ -101,7 +101,7 @@
     public static function getSingletonInstance($dsn = '', $force_run = false, $new = false)
     {
         $objThis = SC_Query_Ex::getPoolInstance($dsn);
-        if (is_null($objThis)) {
+        if ($new || is_null($objThis)) {
             $objThis = SC_Query_Ex::setPoolInstance(new SC_Query_Ex($dsn, $force_run, $new), $dsn);
         }
         /*
Index: tests/class/Common_TestCase.php
===================================================================
--- tests/class/Common_TestCase.php     (リビジョン 23329)
+++ tests/class/Common_TestCase.php     (作業コピー)
@@ -27,14 +27,13 @@
 
   protected function setUp()
   {
-    $this->objQuery = SC_Query_Ex::getSingletonInstance('', true);
+    $this->objQuery = SC_Query_Ex::getSingletonInstance('', true, true);
     $this->objQuery->begin();
   }
 
   protected function tearDown()
   {
     $this->objQuery->rollback();
-    $this->objQuery = null;
   }
 
   /**
Index: tests/require.php.base
===================================================================
--- tests/require.php.base      (リビジョン 23329)
+++ tests/require.php.base      (作業コピー)
@@ -1,6 +1,19 @@
 <?php
 $HOME = realpath(dirname(__FILE__)) . '/../';
 require_once "$HOME/html/require.php";
+
+// PHPUnitのグローバル変数バックアップによるMDB2オブジェクトの破壊防止の為、
+// html/require.php を読み込んだ際に作成されたMDB2オブジェクトを開放する。
+// 
+// @see  PHPUnit_Framework_TestCase::$backupGlobals
+// @todo MDB2を使用しなくなった際に削除する。
+foreach (SC_Query_Ex::$arrPoolInstance as $objQuery) {
+    $objQuery->conn->disconnect();
+    $objQuery->conn->free();
+}
+SC_Query_Ex::$arrPoolInstance = array();
+unset($objQuery);
+
 // ローカルの環境に応じて追加・編集し、phpunitが使えるように設定してください。
 set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/share/pear');
 set_include_path(get_include_path() . PATH_SEPARATOR . '/usr/local/lib/php');



長くなりましたがこの解決方法の是非と、
より良い解決方法がありましたらご教示頂けましたら幸いです。


ついでになって失礼ですが SC_Query_Ex::getSingletonInstance($dsn, $force_run, $new) の $new について、
既にコネクションプールに $dsn が存在する場合は問答無用で新規接続しないという挙動になっています。
これは仕様でしょうか?


----------------
Seiji Nitta
zenith6@gmail.com
https://github.com/zenith6/

スレッド表示 | 新しいものから 前のトピック | 次のトピック | トップ


 



ログイン


EC-CUBE公式 Amazon Payプラグイン

統計情報

総メンバー数は88,295名です
総投稿数は109,692件です

投稿数ランキング

1
seasoft
7365
2
468
3217
3
AMUAMU
2712
4
nanasess
2303
5
umebius
2085
6
yuh
1818
7
h_tanaka
1610
8
red
1568
9
mcontact
1240
10
tsuji
958
11
fukap
907
12
shutta
835
13
tao_s
796
14 ramrun 789
15 karin 689
16 sumida 641
17
homan
633
18 DELIGHT 572
19
patapata
502
20
flealog
485


ネットショップの壺

EC-CUBEインテグレートパートナー

Copyright© EC-CUBE CO.,LTD. All Rights Reserved.