Ethnaを業務で使うために(4) テストの整備

Ethnaを業務で使うために(3) テスト関連のディレクトリ構造の変更 - maru.cc@はてな」の続きです。


前回は、テスト関連のファイルが作成されるディレクトリ変更を行いましたが、次に実際にテストが動くようにします。
移動した状態を元に書きますので、前回の記事を参考にしてください。


ethnaコマンドでテストファイルが作成されるディレクトリの変更は行いましたが、このままでは、テストファイルが読み込まれないので、テストが動きません。
なので、そのための準備を行います。


行う作業

  • Common_UnitTestCaseを作成
    • 特殊な処理をしたい場合や、共通の setUp、tearDown をかけるようにします
    • テストのスケルトンファイルも修正します
  • Common_UnitTestManagerクラスを改造
    • AppManagerのテストも自動で行われるようにします
  • Common_UnitTestReporterクラス作成
    • 文字コード周りの修正や、今後のためにReporterを作成します
    • UnitTestのテンプレートも今後いじるので、testディレクトリの下のを使うようにします
  • Common_Controllerの変更
  • Common_View_UnitTestの作成
    • 設定されているアプリケーションIDとは違う共通マネージャを呼ぶために改造します
    • Common_View_UnitTestを使うための記述を追加します
  • etc/mm-ini.phpを作成
    • 最後にdebugをtrueにして動作確認です

Common_UnitTestCaseを作成

現行の Ethna_UnitTestCase には、連続したテストをした場合に、ActionError や Validator が、ひとつ前の状態を残しているという動作をします。
そのため、テストを実行する順番に依存したり、意図しない動作を起こしてしまうことがありますので、その対応をします。


また、AppAction や Viewの中で、$_SERVER や $_GET、$_POSTを直接操作している場所のテストで直接グローバル変数の中身を書き換える場合に、戻すための処理を共通で入れておいてしまいます。


test/Common_UnitTestCase.php を作成します。

<?php
/**
 *  Common_UnitTestCase.php
 *
 *  @author
 *  @package    Common
 *  @version    $Id: app.unittestcase.php  Exp $
 */

/**
 *  UnitTestCase実行クラス
 *
 *  @author
 *  @access     public
 *  @package    Common
 */
class Common_UnitTestCase extends Ethna_UnitTestCase
{
    var $_tmp = array();

    /**
     *    テストの初期化
     *
     *    @access public
     */
    function setUp()
    {
        $this->_tmp['server'] = $_SERVER;
        $this->_tmp['get']    = $_GET;
        $this->_tmp['post']   = $_POST;
        $this->_tmp['file']   = $_FILE;
    }

    /**
     *    テストの後始末
     *
     *    @access public
     */
    function tearDown()
    {
        $_SERVER = $this->_tmp['server'];
        $_GET    = $this->_tmp['get'];
        $_POST   = $this->_tmp['post'];
        $_FILE   = $this->_tmp['file'];
    }

    /**
     *  アクションフォームの作成と関連付け
     *
     *  @access public
     */
    function _createActionForm($form_name)
    {
        parent::_createActionForm($form_name);

        // action_errorの初期化
        $ae =& $this->ctl->getActionError();
        $ae->clear();
        unset($ae->action_form);

        // Validatorの初期化
        unset($this->ctl->class_factory->object['plugin']->obj_registry["Validator"]);
    }
}
?>


これを使われるようにスケルトンファイルを変更します。
また、初期テストとして、テスト未作成だというのが漏れないようにエラーを追加しておきます。
skel/skel.action_test.php

<?php
/**
 *  {$action_path}
 *
 *  @author     {$author}
 *  @package    Common
 *  @version    $Id:  $
 */

/**
 *  {$action_name}フォームのテストケース
 *
 *  @author     {$author}
 *  @access     public
 *  @package    Common
 */
class {$action_form}_TestCase extends Common_UnitTestCase
{
    /**
     *  @access private
     *  @var    string  アクション名
     */
    var $action_name = '{$action_name}';

    /**
     *    テストの初期化
     *
     *    @access public
     */
    function setUp()
    {
        parent::setUp();
        $this->createActionForm();  // アクションフォームの作成
    }

    /**
     *    テストの後始末
     *
     *    @access public
     */
    function tearDown()
    {
        parent::tearDown();
    }

    /**
     *  アクションフォームのテストケース
     *
     *  @access public
     */
    function test_validate()
    {
        $this->assertNull('error',"テストケース未作成");
    }
}

/**
 *  {$action_name}アクションのテストケース
 *
 *  @author     {$author}
 *  @access     public
 *  @package    Common
 */
class {$action_class}_TestCase extends Common_UnitTestCase
{
    /**
     *  @access private
     *  @var    string  アクション名
     */
    var $action_name = '{$action_name}';

    /**
     *    テストの初期化
     *
     *    @access public
     */
    function setUp()
    {
        parent::setUp();
        $this->createActionForm();  // アクションフォームの作成
        $this->createActionClass(); // アクションクラスの作成

        $this->session->start();            // セッションの開始
    }

    /**
     *    テストの後始末
     *
     *    @access public
     */
    function tearDown()
    {
        $this->session->destroy();      // セッションの破棄
        parent::tearDown();
    }

    /**
     *  アクションクラスのテストケース
     *
     *  @access public
     */
    function test_action()
    {
        $this->assertNull('error',"テストケース未作成");

        // {$action_name}アクション実行前の認証処理
        $forward_name = $this->ac->authenticate();
        $this->assertNull($forward_name);

        // {$action_name}アクションの前処理
        $forward_name = $this->ac->prepare();
        $this->assertNull($forward_name);

        // {$action_name}アクションの実装
        $forward_name = $this->ac->perform();
        $this->assertEqual($forward_name, '{$action_name}');
    }
}
?>


同じく、ViewTestも修正します。
skel/skel.view_test.php

<?php
/**
 *  {$view_path}
 *
 *  @author     {$author}
 *  @package    Common
 *  @version    $Id:  $
 */

/**
 *  {$forward_name}ビューの実装
 *
 *  @author     {$author}
 *  @access     public
 *  @package    Common
 */
class {$view_class}_TestCase extends Common_UnitTestCase
{
    /**
     *  @access private
     *  @var    string  ビュー名
     */
    var $forward_name = '{$forward_name}';

    /**
     *    テストの初期化
     *
     *    @access public
     */
    function setUp()
    {
        parent::setUp();
        $this->createPlainActionForm(); // アクションフォームの作成
        $this->createViewClass();       // ビューの作成
    }

    /**
     *    テストの後始末
     *
     *    @access public
     */
    function tearDown()
    {
        parent::tearDown();
    }

    /**
     *  遷移前処理のテストケース
     *
     *  @access public
     */
    function test_view()
    {
        $this->assertNull('error',"テストケース未作成");

        // {$forward_name}遷移前処理
        $this->vc->preforward();
    }
}
?>

Common_UnitTestManagerクラスを改造

今回の目玉です。
マネージャのテスト対応の Common_UnitTestManager を修正します。


まずは、app以下にあるので、test以下に移動します。

$ mv app/Common_UnitTestManager.php test/


ちょっと長いですが、中身をごっそり以下のように書き換えます。

<?php
/**
 *  Common_UnitTestManager.php
 *
 *  @author
 *  @package    Common
 *  @version    $Id:  $
 */

include_once 'Common_UnitTestReporter.php';
include_once 'Common_UnitTestCase.php';

/**
 *  Commonユニットテストマネージャクラス
 *
 *  @author
 *  @access     public
 *  @package    Common
 */
class Common_UnitTestManager extends Ethna_UnitTestManager
{
    /**
     *  @var    array   一般テストケース定義
     */
    var $testcase = array(
        /*
         *  TODO: ここに一般テストケース定義を記述してください
         *
         *  記述例:
         *
         *  'util' => 'app/UtilTest.php',
         */
    );

    /**
     *  Common_UnitTestManagerのコンストラクタ
     *
     *  @access public
     *  @param  object  Ethna_Backend   &$backend   Ethna_Backendオブジェクト
     */
    function Common_UnitTestManager(&$backend)
    {
        parent::Ethna_UnitTestManager($backend);

        // マネージャのテスト取得($this->testcase優先)
        $testmanager = $this->_getTestManager();
        if (is_array($testmanager) && count($testmanager)) {
            $this->testcase = array_merge($testmanager, (array)$this->testcase);
        }

        $this->ctl->_includeDirectory($this->ctl->getDirectory('test_action'));
        $this->ctl->_includeDirectory($this->ctl->getDirectory('test_action_cli'));
        $this->ctl->_includeDirectory($this->ctl->getDirectory('test_action_xmlrpc'));
        $this->ctl->_includeDirectory($this->ctl->getDirectory('test_view'));
    }

    /**
     * マネージャテストクラスを取得する
     *
     * @access private
     * @return array
     */
    function _getTestManager()
    {
        $testcase = array();
        $manager_path = $this->ctl->directory["test_manager"];
        $prefix = join($this->ctl->plugin_search_appids, '|');
        foreach (glob($manager_path.'/*Test.php') as $file) {
            if (preg_match('/^(('.$prefix.')_.+Manager)Test.php/i', strtolower(basename($file)), $mathces)) {
                $testcase[$mathces[1]] = '/'.$manager_path.'/'.basename($file);
            }
        }
        return $testcase;
    }

    /**
     *  ユニットテストを実行する
     *
     *  @access private
     *  @return mixed   0:正常終了 Ethna_Error:エラー
     */
    function run()
    {
        $action_class_list = $this->_getTestAction();
        $view_class_list = $this->_getTestView();

        $test =& new GroupTest("Ethna UnitTest");

        // アクション
        foreach ($action_class_list as $action_name) {
            $action_class = $this->ctl->getDefaultActionClass($action_name, false).'_TestCase';
            $action_form = $this->ctl->getDefaultFormClass($action_name, false).'_TestCase';

            $test->addTestCase(new $action_class($this->ctl));
            $test->addTestCase(new $action_form($this->ctl));
        }

        // ビュー
        foreach ($view_class_list as $view_name) {
            $view_class = $this->ctl->getDefaultViewClass($view_name, false).'_TestCase';

            $test->addTestCase(new $view_class($this->ctl));
        }

        // 一般
        foreach ($this->testcase as $class_name => $file_name) {
            if(!strlen($target) || substr_count($class_name,$target)){
                $dir = $this->ctl->getBasedir().'/';
                if(file_exists_ex($file_name)){
                    include_once $file_name;
                }elseif(is_file($dir . $file_name)){
                    include_once $dir . $file_name;
                }else{
                    trigger_error("マネージャーテスト取得失敗");
                }

                $testcase_name = $class_name.'_TestCase';
                $test->addTestCase(new $testcase_name($this->ctl));
            }
        }

        // ActionFormのバックアップ
        $af =& $this->ctl->getActionForm();

        //出力したい形式にあわせて切り替える
        $reporter = new Common_UnitTestReporter();
        $test->run($reporter);

        // ActionFormのリストア
        $this->ctl->action_form =& $af;
        $this->backend->action_form =& $af;
        $this->backend->af =& $af;

        return array($reporter->report, $reporter->result);
    }
}
?>

Common_UnitTestReporterクラス作成

Common_UnitTestManagerですでに読み込んでいますが、出力形式を変更したりとか、後ほど使うことになるので、Ethna_UnitTestReporter を継承して新規作成します。

追加する機能としては、mbstring.http_output を変更します。
うちでは、携帯案件が多いので、http_output を SJISにしているのですが、それとは関係なく、テストでは、指定された文字コードで出力するようにします。
また、パスしたテストも一覧で確認したい時のために、unitetest.php?all=1 というパラメータ時にメッセージを出すような機能を追加します。


test/Common_UnitTestReporter.php 新規作成

<?php
/**
 *  Common_UnitTestReporter.php
 *
 *  @author
 *  @package    Common
 *  @version    $Id:   Exp $
 */

/**
 *  Common_UnitTestReporter
 *
 *  @author
 *  @access     public
 *  @package    Common
 */
class Common_UnitTestReporter extends Ethna_UnitTestReporter {

    var $_character_set;

    var $report;
    var $result;

    /**
     *  Common_UnitTestReporterのコンストラクタ
     *
     *  @access public
     *  @param  string  $character_set  キャラクタセット
     */
    function Common_UnitTestReporter($character_set = 'EUC-JP')
    {
        ini_set('mbstring.http_output', $character_set);
        parent::Ethna_UnitTestReporter($character_set);
    }

    /**
     *  パス
     *
     *  @access public
     * @param string   $message    メッセージ
     */
    function paintPass($message)
    {
        parent::paintPass($message);

        if (!$_GET['all']) {
            array_pop($this->report);
        }
    }
}
?>


テスト用のテンプレートは、デフォルトでは、lib/Ethna/tpl/unittest.tpl が使われますが、今後変更したい場合に、libの下をいじるのはよくないので、testディレクトリの下にテンプレートを持ってきてしまいます。

$ cp lib/Ethna/tpl/unittest.tpl test/

Common_View_UnitTestの作成

ここまで準備してきた Common_UnitTestManager ですが、残念ながらそのままでは読み込んでくれません。
Ethnaデフォルトの機能として、アプリケーションIDのついた UnitTestManager を読み込む仕様なので、Commonという名前で呼び出したい場合には、拡張が必要です。
他のプラグインのように、$plugin_search_appids を見てくれればいいのですが。。


ついでに、Smartyのデリミタも、JSの括弧と被らないようにサイト側で変えている場合がありますので、テスト用テンプレートでは「{ }」になるように設定します。


とりあえず、アプリケーションIDのところだけ、直値で指定しちゃったViewを作成します。
test/Common_View_UnitTest.php 新規作成

<?php
/**
 *  Common_View_UnitTest.php
 *
 *  @author
 *  @license    http://www.opensource.org/licenses/bsd-license.php The BSD License
 *  @package    Ethna
 *  @version    $Id:  $
 */

/**
 *  __ethna_unittest__ビューの実装
 *
 *  @author
 *  @access     public
 *  @package    Ethna
 */
class Common_View_UnitTest extends Ethna_ViewClass
{
    /**
     *  共通値を設定する
     *
     *  @access protected
     *  @param  object  Ethna_Renderer  レンダラオブジェクト
     */
    function _setDefault(&$renderer)
    {
        $smarty =& $renderer->engine;
        $smarty->left_delimiter  = "{";
        $smarty->right_delimiter = "}";
    }

    /**
     *  遷移前処理
     *
     *  @access public
     */
    function preforward()
    {
        // タイムアウトしないように変更
        $max_execution_time = ini_get('max_execution_time');
        set_time_limit(0);

        if (!headers_sent()) {
            // キャッシュしない
            header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
            header("Last-Modified: " . gmdate("D, d M Y H:i:s \G\M\T"));
            header("Cache-Control: no-store, no-cache, must-revalidate");
            header("Cache-Control: post-check=0, pre-check=0", false);
            header("Pragma: no-cache");
        }

        $ctl =& Ethna_Controller::getInstance();

        // cores
        $this->af->setApp('app_id', $ctl->getAppId());
        $this->af->setApp('ethna_version', ETHNA_VERSION);

        // include
        $file = sprintf("%s/%s_UnitTestManager.php",
            $ctl->getDirectory('test'),
            'Common');
        include_once $file;

        // run
        $r = sprintf("%s_UnitTestManager", 'Common');
        $ut =& new $r($this->backend);
        list($report, $result) = $ut->run();

        // result
        $this->af->setApp('report', $report);
        $this->af->setApp('result', $result);

        // タイムアウトを元に戻す
        set_time_limit($max_execution_time);
    }
}
?>

ほとんど、lib/Ethna/class/View/Ethna_View_UnitTest.php のままです。

Common_Controllerの変更

最後に、いま作った Common_View_UnitTest が使用されるように、Controllerを修正します。
app/Common_Controller.php 新しいメソッドを追加
268行目あたり追記します。
修正前

    /**#@-*/
}
?>

修正後

    /**#@-*/

    /**
     *  Ethnaマネージャを設定する
     *
     *  @access protected
     */
    function _activateEthnaManager()
    {
        if ($this->config->get('debug') == false) {
            return;
        }
        parent::_activateEthnaManager();

        // forward設定
        $this->forward['__ethna_unittest__'] = array(
            'forward_path'  => sprintf('%s/test/unittest.tpl', BASE),
            'view_name'     => 'Common_View_UnitTest',
            'view_path'     => sprintf('%s/test/Common_View_UnitTest.php', BASE),
        );
    }
}
?>

etc/mm-ini.phpを作成

最後に、iniの設定をします。
今回は、「mm」というアプリケーションIDで作成していますので、以下のようにcommonファイルの名前を変更します。
もちろん、他の名前で作成している場合には、そちらを使用してください。

$ mv etc/common-ini.php etc/mm-ini.php


UnitTestを動かすためには、debug設定を true にする必要があります。
etc/mm-ini.php 13行目あたり

    // debug
    // (to enable ethna_info and ethna_unittest, turn this true)
    'debug' => true,

動かしてみる

実際にテストを動かしてみます。
まずは、マネージャの作成。

$ sh ethna.sh add-app-manager sample
file generated [/path/to/common/lib/Ethna/skel/skel.app_manager.php -> /path/to/common/app/manager/Mm_SampleManager.php]
app-manager script(s) successfully created [/path/to/common/app/manager/Mm_SampleManager.php]


マネージャのテスト作成。
ethnaコマンドにテスト作成のコマンドが無いので、手で作成します。ethna add-app-manager-test とか追加したいですね。
まず、ディレクトリ作成

$ mkdir test/manager

test/manager/Mm_SampleManagerTest.php 新規作成

<?php
/**
 *  Mm_SampleManagerTest.php
 *
 *  @author     {$author}
 *  @package    Mm
 *  @version    $Id:  Exp $
 */

/**
 *  mobile_indexビューの実装
 *
 *  @author     {$author}
 *  @access     public
 *  @package    Mm
 */
class Mm_SampleManager_TestCase extends Common_UnitTestCase
{
    /**
     *  @access private
     *  @var    string  マネージャ名
     */
    var $manager_name = 'sample';

    /**
     *  @access private
     *  @var    object  マネージャ
     */
    var $manager;

    function setUp()
    {
        parent::setUp();
        $this->manager = $this->backend->getManager($this->manager_name);

        //セッションの開始
        $this->session->start();            // セッションの開始
    }

    /**
     *    テストの後始末
     *
     *    @access public
     */
    function tearDown()
    {
        $this->session->destroy();      // セッションの破棄
        parent::tearDown();
    }

    /**
     * テスト
     */
    function test_sample(){
        $this->assertNull('test');
    }
}
?>


Action と Viewも作成。
元々あるIndexアクションは、アプリケーションIDが Commonなのでいったん削除してから新規作成します。

$ rm app/action/Index.php
$ rm app/view/Index.php

新規作成

$ sh ethna.sh add-action index
file generated [/path/to/common/skel/skel.action.php -> /path/to/common/app/action/Index.php]
action script(s) successfully created [/path/to/common/app/action/Index.php]
$ sh ethna.sh add-action-test index
file generated [/path/to/common/skel/skel.action_test.php -> /path/to/common/test/action/IndexTest.php]
action test(s) successfully created [/path/to/common/test/action/IndexTest.php]
$ sh ethna.sh add-view index
file generated [/path/to/common/skel/skel.view.php -> /path/to/common/app/view/Index.php]
view script(s) successfully created [/path/to/common/app/view/Index.php]
$ sh ethna.sh add-view-test index
file generated [/path/to/common/skel/skel.view_test.php -> /path/to/common/test/view/IndexTest.php]
view test(s) successfully created [/path/to/common/test/view/IndexTest.php]


ここまで作業して、各ディレクトリの中は、以下のようになっていると思います。

$ ls app/
Common_ActionClass.php  Common_Controller.php  Common_UrlHandler.php  action      action_xmlrpc  manager  view
Common_ActionForm.php   Common_Error.php       Common_ViewClass.php   action_cli  filter         plugin
$ ls app/action
Index.php
$ ls app/view/
Index.php
$ ls etc/
mm-ini.php
$ ls test/
Common_UnitTestCase.php     Common_UnitTestReporter.php  action    manager       unittest.tpl
Common_UnitTestManager.php  Common_View_UnitTest.php     info.php  unittest.php  view
$ ls test/action/
IndexTest.php
$ ls test/manager/
Mm_SampleManagerTest.php
$ ls test/view/
IndexTest.php


test/unittest.php にアクセスして動作確認をしてみます。
うちの開発環境の場合、ユーザディレクトリ内の public_htmlにおいているので、直接アクセスできますが、そうではない場合には、公開ディレクトリに unittest.php を、コピーかシンボリックリンク作成などをして確認してみてください。


以下のようにエラーが発生すれば成功です。

次回

コントローラー周りの整備を書きたいと思います。