zz log

zaininnari Blog

CakePHP1.3 CakeTestCase の拡張を読み込む bootstrap 的なものをつくる

テストケースを作成する際に、拡張元となる CakeTestCase を拡張します。
CakeTestCase を拡張した独自のクラスを作成することで、
共通の処理を追加することができます。


しかし、残念ながら、
CakeTestCase クラスの読み込みには、
app_controller.php のような中継するクラスは用意されていません。


そのため、
共通処理を書いた CakeTestCase の拡張を読み込む場所が少々面倒なことになります。

  • [app/tests/cases/controllers/users_controller.test.php]などの先頭で、毎回読み込むのが面倒
  • [app/config/bootstrap.php]に、テストための処理を書くのは憚られる
  • コアハックは論外

ので、その拡張クラスを読み込むテスト用の bootstrap.php 的なものをまず作ります。


CakeTestCase クラスは、
app_controller.php のような空っぽのクラスは用意されていません。

前提 & 環境

  • テストを実行するのは、コンソール「cake testsuite app all」です。
    • example.com/test.php の web経由のアクセスは対象外です。
  • 作業環境
  • 意識する対象
    • PHP5.2 以上の動作を意識します。
      • PHP5.2 でのチェックはしていませんが、PHP5.3 の機能は使用していません。
      • PHP5 の機能(アクセス権など)は、使用します。
    • CakePHP 1.3 以上の動作を意識します。
      • 安定化・高速化が図られたバージョンを選択しています。

参考 : CakeTestCase クラスの読み込み場所

[cake/tests/lib/test_manager.php]

<?php
/**
 * Includes the required simpletest files in order for the testsuite to run
 *
 * @return void
 * @access public
 */
  function _installSimpleTest() {
    App::import('Vendor', array(
      'simpletest' . DS . 'unit_tester',
      'simpletest' . DS . 'mock_objects',
      'simpletest' . DS . 'web_tester'
    ));
// ここで基礎となるクラスを読み込みます。
    require_once(CAKE_TESTS_LIB . 'cake_web_test_case.php');
    require_once(CAKE_TESTS_LIB . 'cake_test_case.php');
  }

CAKE_TESTS_LIB の定義

<?php
/**
 * Path to the core tests directory.
 */
if (!defined('CAKE_TESTS')) {
  define('CAKE_TESTS', CAKE.'tests'.DS);
}

/**
 * Path to the test suite.
 */
  define('CAKE_TESTS_LIB', CAKE_TESTS.'lib'.DS);

解説

本題と同時に、コンソールでのテストを実行前までも解説します。

「cake testsuite app all」を例とします。
[cake/console/cake] から、[cake/console/cake.php] に振り返られます。
ShellDispatcher::ShellDispatcher が呼ばれ、
コンソールを操作する上で必要なファイルを読み込みます。

[cake/console/cake.php]

<?php
  function ShellDispatcher($args = array()) {
    set_time_limit(0);

    $this->__initConstants();
    $this->parseParams($args);
    $this->_initEnvironment();
    $this->__buildPaths();
// メイン処理は、$this->dispatch() が行います。
    $this->_stop($this->dispatch() === false ? 1 : 0);
  }
    
  function dispatch() {
// 略
// schema 処理なら、SchemaShell クラスインスタンスを、
// testsuite 処理なら、TestSuiteShell クラスインスタンスを、
// というように、その処理用のクラスを読み込みます。
// ここでは、 
// TestSuiteShell クラスインスタンスを取得します。
// また、$plugin は 「null」です。
    $Shell = $this->_getShell($plugin);
// 略
      if (method_exists($Shell, 'main')) {
// 専用クラスの処理を開始します。
        $Shell->startup();
        return $Shell->main();
      }
// 略
  }

$Shell = $this->_getShell($plugin) を詳しく見ます。

[cake/console/cake.php]

<?php
  function _getShell($plugin = null) {
// $this->shellPaths には
// app や cake 、plugin などにあるシェル用ディレクトリへのパスが格納されています。
// cake より app のディレクトリパスが前にあるので、
// パスに目的のファイルがあった場合、そちらが優先されることになります。
// 例 :
// array(
//   '/path/app/plugins/debug_kit/vendors/shells/',
//   '/path/app/vendors/shells/',
//   '/path/vendors/shells/',
//   '/path/cake/console/libs/',
// )
//
// $this->shell には、 実行するシェルの種類が格納されています。
// ここでは、「testsuite」です。
// $this->shellClass には、 実行するシェルのクラス名が格納されています。
// ここでは、「TestsuiteShell」です。
    foreach ($this->shellPaths as $path) {
// パスを作成します。
      $this->shellPath = $path . $this->shell . '.php';
// プラグインは、ここでは無視します。
      $pluginShellPath =  DS . $plugin . DS . 'vendors' . DS . 'shells' . DS;
// プラグイン部分は、ここでは、「true」が返ります。
// 作成されたパスにファイルが存在する場合、ループが止まります。
// ここでは、なにもしなければ、最終的に、
// [/path/cake/console/libs/testsuite.php] でループが止まりますが、
// [/path/app/vendors/shells/testsuite.php] や、
// [/path/vendors/shells/testsuite.php] が存在する場合、
// そちらでループが止まるようになります。
// 
// [/path/app/vendors/shells/testsuite.php] を作成することで、
// app 側が読み込まれるようになり、再度、[/path/cake/console/libs/testsuite.php] を読み込むことで、
// bootstrap.php 的な使い方ができるようになります。
      if ((strpos($path, $pluginShellPath) !== false || !$plugin) && file_exists($this->shellPath)) {
        $loaded = true;
        break;
      }
    }
    if (!isset($loaded)) {
      return false;
    }

    if (!class_exists('Shell')) {
      require CONSOLE_LIBS . 'shell.php';
    }

    if (!class_exists($this->shellClass)) {
      require $this->shellPath;
    }
    if (!class_exists($this->shellClass)) {
      return false;
    }
    $Shell = new $this->shellClass($this);
    return $Shell;
  }

[/path/vendors/shells/testsuite.php] を作成します。
CakeTestCase の拡張クラスとして、
WhiskCakeTestCase クラスを作成したとします。
(Whisk : 泡立て器 -> http://www.google.com/images?hl=ja&safe=off&q=whisk&um=1&ie=UTF-8 )


[/path/vendors/shells/testsuite.php]

<?php
// 元々読み込むはずだった [/path/cake/console/libs/testsuite.php] を読み込みます。
require_once CONSOLE_LIBS . 'testsuite.php';

// これはおまじないです。
// CakeTestCase クラスの読み込み前に読み込まないと
// なぜか、simpletest の HtmlReporter クラスが多重定義で怒られます。
// おなじないがないと、
// [/path/cake/console/libs/testsuite.php] の __installSimpleTest メソッドの
// 全く同じ記述の App::import('Vendor', 'simpletest' . DS . 'reporter') で発生します。
// CakePHP2 では、simpletest から PHPUnit にテストが変更になるので、
// 今のところ回避だけにしています。
App::import('Vendor', 'simpletest' . DS . 'reporter');

// 本命の CakeTestCase の拡張クラスを読み込みます。
// ファイルパスは、
// [/path/cake/tests/lib/cake_test_case.php] に倣って
// [/path/app/tests/lib/whisk_cake_test_case.php] です。
App::import(null, 'WhiskCakeTestCase', false, TESTS . 'lib');

[app/tests/lib/whisk_cake_test_case.php]

<?php
// 親クラスを読み込みます。
App::import(null, 'CakeTestCase', false, CAKE_TESTS_LIB);

class WhiskCakeTestCase extends CakeTestCase
{
// 共通処理
}