はじめに
最近 Qiita に↓このようなものを投稿しましたが、どちらも Zend Framework 2 を使っていて気づいたものです。
- PHPUnit の TestCase のメンバはテストが完了するまで解放されない - Qiita [キータ]
- PHP - SPLのコンテナクラスで循環参照するとGCで回収されない - Qiita [キータ]
Zend Framework 2 でテストの書き方を調べていたところ、普通にテストを書くとテストの数だけ DB 接続が行われ、最終的に MaxConnections に達してテストがコケます。
PHPUnit の方は Zend Framework 1 にも影響します。まずは Zend Framework 1 について書きます。
QuickStart で問題を発生させる
アプリケーションのコードには、↓にある QuickStart のソースコードを使います。
そのままだと DB が sqlite なので、schema.sqlite.sql を参考に MySQL にテーブルを作成します。
CREATE TABLE guestbook ( id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, email VARCHAR(32) NOT NULL DEFAULT 'noemail@test.com', comment TEXT NULL, created DATETIME NOT NULL );
application.ini で DB を MySQL に変更します。お試しなので production/testing/development は同じでも構いません。
application/configs/application.ini
[production] : resources.db.adapter = "PDO_MYSQL" resources.db.params.dbname = "zf1 " resources.db.params.host = "localhost" resources.db.params.charset = "utf8" resources.db.params.username = "test" resources.db.params.password = "pass" : [testing : production] : ;resources.db.adapter = "PDO_SQLITE" ;resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-testing.db" [development : production] : ;resources.db.adapter = "PDO_SQLITE" ;resources.db.params.dbname = APPLICATION_PATH "/../data/db/guestbook-dev.db"
テストのための bootstrap を作ります。
tests/application/bootstrap.php
<?php define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../../application')); define('APPLICATION_ENV', 'testing'); set_include_path(implode(PATH_SEPARATOR, array( realpath(APPLICATION_PATH . '/../library'), get_include_path(), ))); require_once 'Zend/Application.php'; require_once 'Zend/Test/PHPUnit/ControllerTestCase.php';
次のように GuestbookController のテストを作ります。
tests/application/controllers/GuestbookControllerTest.php
<?php class GuestbookControllerTest extends Zend_Test_PHPUnit_ControllerTestCase { public function setUp() { $this->bootstrap = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini' ); parent::setUp(); } public function test() { $this->dispatch('/guestbook'); //$this->assertResponseCode(200); // E_DEPRECATED $this->assertEquals(200, $this->getResponse()->getHttpResponseCode()); } }
assertResponseCode を使っていないのは PHPUnit の最新版だと E_DEPRECATED が発生するためです。Zend Framework 1 はもうオワコンですね。
次に、一度テストを実行して、テストが OK なこと、MySQL のログにアプリケーションからの接続が記録されていること、を確認します。
$ phpunit --bootstrap tests/application/bootstrap.php tests/application/controllers/ PHPUnit 3.7.27 by Sebastian Bergmann. . Time: 513 ms, Memory: 4.75Mb OK (1 test, 1 assertion) $ sudo tail -n 5 /var/log/mysql/query.log 131005 17:00:40 1763 Connect test@127.0.0.1 on zf1 1763 Query SET NAMES 'utf8' 1763 Query DESCRIBE `guestbook` 1763 Query SELECT `guestbook`.* FROM `guestbook` 1763 Quit
テストを次のように書き換えます。@dataProvider アノテーションを使ってテストを 200 回実行させます。
tests/application/controllers/GuestbookControllerTest.php
<?php class GuestbookControllerTest extends Zend_Test_PHPUnit_ControllerTestCase { public function setUp() { $this->bootstrap = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini' ); parent::setUp(); } /** * @dataProvider data */ public function test() { $this->dispatch('/guestbook'); //$this->assertResponseCode(200); // E_DEPRECATED $this->assertEquals(200, $this->getResponse()->getHttpResponseCode()); } public function data() { return array_fill(0, 200, array()); } }
テストを実行します。
$ phpunit --bootstrap tests/application/bootstrap.php tests/application/controllers/ PHPUnit 3.7.27 by Sebastian Bergmann. ............................................................... 63 / 200 ( 31%) ............................................................... 126 / 200 ( 63%) ..........................FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 189 / 200 ( 94%) FFFFFFFFFFF Time: 1.19 minutes, Memory: 18.00Mb There were 48 failures: 1) GuestbookControllerTest::test Failed asserting that 500 matches expected 200. : : : FAILURES! Tests: 200, Assertions: 200, Failures: 48.
MySQL の設定によりますが、途中で MaxConnections に達してテストがコケます。
解決方法:tearDown で解放
Zend_Application_Resource_Db
がインスタンス化した Zend_Db_Adapter
を、Zend_Application
(Zend_Application_Bootstrap_Bootstrap
)が掴んでいて、かつ、Zend_Test_PHPUnit_ControllerTestCase
が Zend_Application
をテストの完了まで掴んで離さないことが原因なので、次のように tearDown を書いてやれば GC のタイミングで DB 接続も解放されます。
<?php class GuestbookControllerTest extends Zend_Test_PHPUnit_ControllerTestCase { public function setUp() { $this->bootstrap = new Zend_Application( APPLICATION_ENV, APPLICATION_PATH . '/configs/application.ini' ); parent::setUp(); } public function tearDown() { parent::tearDown(); $this->reset(); $this->bootstrap = null; gc_collect_cycles(); } /** * @dataProvider data */ public function test() { $this->dispatch('/guestbook'); //$this->assertResponseCode(200); // E_DEPRECATED $this->assertEquals(200, $this->getResponse()->getHttpResponseCode()); } public function data() { return array_fill(0, 200, array()); } }
次のとおりです。テストの都度、接続~切断を繰り返します。
$ phpunit --bootstrap tests/application/bootstrap.php tests/application/controllers/ PHPUnit 3.7.27 by Sebastian Bergmann. ............................................................... 63 / 200 ( 31%) ............................................................... 126 / 200 ( 63%) ............................................................... 189 / 200 ( 94%) ........... Time: 1.18 minutes, Memory: 6.00Mb OK (200 tests, 200 assertions) $ sudo tail -n 15 /var/log/mysql/query.log 131005 17:10:12 210 Connect test@127.0.0.1 on zf1 210 Query SET NAMES 'utf8' 210 Query DESCRIBE `guestbook` 210 Query SELECT `guestbook`.* FROM `guestbook` 210 Quit 211 Connect test@127.0.0.1 on zf1 211 Query SET NAMES 'utf8' 211 Query DESCRIBE `guestbook` 211 Query SELECT `guestbook`.* FROM `guestbook` 211 Quit 212 Connect test@127.0.0.1 on zf1 212 Query SET NAMES 'utf8' 212 Query DESCRIBE `guestbook` 212 Query SELECT `guestbook`.* FROM `guestbook` 212 Quit
解決方法:DB接続をグローバルに持つ
そもそもテストの都度 DB に接続する方がバカげているので、Bootstrap で db リソースの初期化を変えた方が良いです。
<?php class Bootstrap extends Zend_Application_Bootstrap_Bootstrap { : protected function _initDb() { if (!Zend_Db_Table::getDefaultAdapter()) { $options = $this->getOptions(); $adapter = $options['resources']['db']['adapter']; $params = $options['resources']['db']['params']; $db = Zend_Db::factory($adapter, $params); Zend_Db_Table::setDefaultAdapter($db); } return Zend_Db_Table::getDefaultAdapter(); } }
毎回接続しない分、この方がテストの実行もだいぶ早いです。
$ phpunit --bootstrap tests/application/bootstrap.php tests/application/controllers/ PHPUnit 3.7.27 by Sebastian Bergmann. ............................................................... 63 / 200 ( 31%) ............................................................... 126 / 200 ( 63%) ............................................................... 189 / 200 ( 94%) ........... Time: 5.54 seconds, Memory: 11.25Mb OK (200 tests, 200 assertions) $ cat /var/log/mysql/query.log : 131005 17:14:16 216 Connect test@127.0.0.1 on zf1 216 Query SET NAMES 'utf8' 216 Query DESCRIBE `guestbook` 216 Query SELECT `guestbook`.* FROM `guestbook` 216 Query DESCRIBE `guestbook` 216 Query SELECT `guestbook`.* FROM `guestbook` : 131005 17:14:21 216 Query DESCRIBE `guestbook` 216 Query SELECT `guestbook`.* FROM `guestbook` 216 Query DESCRIBE `guestbook` 216 Query SELECT `guestbook`.* FROM `guestbook` 216 Quit
↑の例では Zend_Db_Table::setDefaultAdapter()
を使いましたが、Zend_Registry
を使ってもいいと思います。
<?php class Bootstrap extends Zend_Application_Bootstrap_Bootstrap { : protected function _initDb() { if (!Zend_Registry::isRegistered('db')) { $options = $this->getOptions(); $adapter = $options['resources']['db']['adapter']; $params = $options['resources']['db']['params']; $db = Zend_Db::factory($adapter, $params); Zend_Registry::set('db', $db); Zend_Db_Table::setDefaultAdapter($db); } $db = Zend_Registry::get('db'); return $db; } }
ちなみに私は Zend_Registry
に近い方法をこれまで使ってきました(Zend_Registry
ではなく独自のシングルトンでしたけど)。
さいごに
普通に Web アプリで実行する分には、リクエストの終了時にすべてのリソースが解放されるので問題にはなりませんが、Zend_Application
や Zend_Application_Resource_Db
や Zend_Test_PHPUnit_ControllerTestCase
など、なんとなく標準っぽいやり方でむしろ問題にぶち当たってしまうのが罠っぽいなーと思います。
なお、Zend Framework 2 だと SplPriorityQueue の問題も絡んできます。近いうちに Zend Framework 2 についても書きます。→ Zend Framework 2 でテストごとにDB接続してコネクションが枯渇する - ngの日記