CakePHP Advent Calendar 2019 - Qiitaの10日目です!!!
皆さんはCakePHPユーザーの皆さん、テストを書いてますでしょうか!
「良いテスト」のためのポイントの1つは「何がしたいのかが判然としている」ことですね。そのためにも「書かなくて良いことを書かない」のは非常に有効です。
以下のようなメリットがあります
- コード量が減る
- 読むべき量が減れば、全体を理解するコストが減る
- テストケースごとの「本質」が浮かび上がる
- そのテストケース独自の処理のみが記述されていることが理想
これらを低コストに実現するためにはどうすればよいでしょうか?
1つは、TestSuiteの力をふんだんに活かしきることです!
ということで、今回はCakePHPのTestSuiteの中身に着目して、「実はTestCaseやTestClassに書かなくて良いこと」を整理してみたいと思います!
1. PHPUnit本体の機能
backupGlobals
PHPUnitには、(スーパー)グローバル変数に関して「テストケースごとにリセットする」という機能があります。
https://phpunit.readthedocs.io/ja/latest/configuration.html
PHPUnit6以上かつcakephp/appのphpunitxml.dist
をそのまま利用している場合、この機能は無効になっているかもしれません。(コマンド実行時の起動オプションで有効化することも出来ます)
もしtearDown/tearDownAfterClass等で「グローバルな状態に関心がある」処理を書いていて場合、これを上手く活用することで、テストコードをスッキリさせることが出来ます
2. CakePHPの機能
FixtureManager
CakePHP3において、FixtureManagerはPHPUnitのリスナーの機能を用いて実行されています。
<!-- Setup a listener for fixtures -->
<listeners>
<listener
class=“\Cake\TestSuite\Fixture\FixtureInjector”>
<arguments>
<object class=“\Cake\TestSuite\Fixture\FixtureManager” />
</arguments>
</listener>
</listeners>
ここでは詳細は割愛しますが、PHPUnitにおけるTestListenerは「テストの進行状態を監視するもの」です。
マニュアル中では以下のページで言及があります。
https://phpunit.readthedocs.io/ja/latest/extending-phpunit.html#phpunit-framework-testlistener
やや困ったことに、「これが何であるか」の説明が端折られているために難しく感じられるかもしれません。が、同ページ中に掲載されている「Example」を見ると、大体「どのようなものか」「どういった時にほしいか」はイメージが掴めるのではないでしょうか。
その他の実装例は、Packagist等を参考に探してみてください。
https://packagist.org/?query=listener&tags=phpunit
さて、Fixtureの管理のために読み込ませているのが FixtureInjector
です。このリスナーには、startTestSuite
endTestSuite
startTest
endTest
の4つのメソッドが実装されています。このうち、「テストケースごと」の処理を行うのは (start|end)Testです。
どちらも非常にシンプルな記述になっているので、そのままコードを示します。
/**
* Adds fixtures to a test case when it starts.
*
* @param \PHPUnit\Framework\Test $test The test case
* @return void
*/
public function startTest(Test $test)
{
$test->fixtureManager = $this->_fixtureManager;
if ($test instanceof TestCase) {
$this->_fixtureManager->fixturize($test);
$this->_fixtureManager->load($test);
}
}
/**
* Unloads fixtures from the test case.
*
* @param \PHPUnit\Framework\Test $test The test case
* @param float $time current time
* @return void
*/
public function endTest(Test $test, $time)
{
if ($test instanceof TestCase) {
$this->_fixtureManager->unload($test);
}
}
身も蓋もない言い方になりますが、「Fixtureの用意/後処理を行う」というのが直感的に見て取れるのではないでしょうか。その内実については、FixtureManagerの実装を追ってみてください。
https://github.com/cakephp/cakephp/blob/3.8.0/src/TestSuite/Fixture/FixtureManager.php
TestCase
最後に、 Cake\TestSuite\TestCase
の中身です。
こちらについては、普段から見慣れているであろう setUp()
tearDown()
の内容を解剖することになります。
百聞は一見にしかずということで、実コードを示します。
public function setUp()
{
parent::setUp();
if (!$this->_configure) {
$this->_configure = Configure::read();
}
if (class_exists('Cake\Routing\Router', false)) {
Router::reload();
}
EventManager::instance(new EventManager());
}
/**
* teardown any static object changes and restore them.
*
* @return void
*/
public function tearDown()
{
parent::tearDown();
if ($this->_configure) {
Configure::clear();
Configure::write($this->_configure);
}
$this->getTableLocator()->clear();
}
まずは setUp()
の中身です
$this->_configure = Configure::read();
これはbackupGlobals
の概念に近く、CakePHPのConfigureクラスの中身を「バックアップ」しています。すなわち、基本的にはtestCaseごとにbootstrap直後の内容が保持できているようなイメージです。1
Router::reload();
こちらは名前の通り、Routerにセットされた内容をリロードする処理です。
具体的には、 Router::$_collection
の内容がまっさらな RouteCollection
インスタンスに更新されます。
EventManager::instance(new EventManager());
EventManagerは、色々なlistnersを保持するマネージャークラスですが、setUp()の内部でまっさらなインスタンスで上書きされるように処理されています。
メソッド名こそinstance()
ですが、ここではインスタンスの取得を目的しているわけではありません。これはEventManagerインスタンスを渡された場合にそれを登録するように振る舞う実装となっているため、その作用を狙っている記述です。
最後に tearDown()
を見ていきます。
Configure::clear();
Configure::write($this->_configure);
これは、先ほど見た通り「改変されたConfigureを初期化する」というものですね。
こういう処理をTestSuite側で持っているので、サブクラス(テストケースクラス)において、例えばtearDown()で「Configure::write((array)$初期値)」という処理は不要です。
実際、コアコードのテストではRouteTest
において「setUp()中で処理しているけど後処理は特にしていない」という記述が出てきます。
https://github.com/cakephp/cakephp/blob/3.8.0/tests/TestCase/Routing/Route/RouteTest.php#L52
$this->getTableLocator()->clear();
TableLocatorで読み込まれたintanceのマップやconfig等を一掃するように処理しています。
内容が気になる方は、 srcを御覧ください。
これにより、何かモデル(Tableインスタンス)を読み込んだ後でも、次時のテストではまっさらな状態でTableLocatorを利用することが可能という算段です。
まとめ
今回の主要な内容は、EventManagerやConfigureに関する部分かな?と個人的には思っています。
しかし、これらについては実はBookを注視すると言及されている内容です。
https://book.cakephp.org/3/ja/development/testing.html#id6
恐らく、Bookは「Cakeを書き始めた時に読んだきり」という方も多く、当時は気付いていなかった〜という話も多いのではないかな?と思います。かくいう私も「Configureが都度リセットされている」ことに気付いたのは本格的にCake3を触り始めてから暫く経った後でした・・・w
ということで、「改めて普段使っている処理を読んで見る」「Bookを詳しく読み返してみる」というのは、フレームワークをより良く使うための意義深い道であるように思います。
それらを使いこなし、理解した上で「テストコードを簡潔に保つ、冗長にさせない」という態度で臨めたら素敵ではないでしょうか。保守性も可読性も高いテストを目指したものです。
CakePHP Advent、明日は @kzkamago0721 さんの記事です!!🎉
-
個人的には、この内容であればsetUpBeforeClass()にて静的にもたせても問題ないような気もしたのですが、どうなのでしょうね。 ↩