Monthly Archives: 7月 2011

fixtureがテスト用データベースに反映されない?

このエントリーをはてなブックマークに追加
はてなブックマーク - fixtureがテスト用データベースに反映されない?
Share on Facebook

CakePHPでフィクスチャを作成したけど、テスト実行時にフィクスチャではなくDBのデータをロードしてしまう。ちゃんとテストケース内で $fixtures に設定しているはずなのに。

そんな症状に長らく悩んでいましたが、結論から言うと、$fixturesで指定しているフィクスチャ名を書き間違えていたせいでした。ファイル名を間違えている時、テストケースはエラーを吐かず、DBを読みに行ってしまうようです。

ずいぶん悩みましたが、初歩的なミスでした。

確かめた記事

Mark Story「1.2のNightlyリビジョンで直ってるよ」

→解決してない

「フィクスチャがDBに入らないんだけど」

「テスト動かせば自動で入るよ」

「知ってるよ。そのはずなのに入らないから聞いてるんだ」

返事無し

fixtureをディレクトリに分けて管理する

このエントリーをはてなブックマークに追加
はてなブックマーク - fixtureをディレクトリに分けて管理する
Share on Facebook

(2011/07/15 Update) 7/13以前のコードに誤りがあり、正しく動作しなかったので修正しました。

やりたいこと

CakePHPの規則に従うと、フィクスチャは /app/test/fixtures/ ディレクトリにすべて入れなければなりません。この場合、「同じモデルを使うけど、テストケース毎に異なるフィクスチャを代入したい」という要望をかなえるのが難しいです。

例えば、

  • Hige コントローラー
  • Moja コントローラー

どちらも同じ Uso モデルを使う場合を考えます。それぞれで違うフィクスチャを使いたくても、Uso モデルのフィクスチャは uso_fixture.php の1個です。フィクスチャの値を両方のテストケースで使えるようにするのは難しく、まして複数人でテスト設計するのは困難です。

そこで、それぞれのコントローラーから呼び出すフィクスチャを

  • /app/test/fixtures/hige/uso_fixture.php
  • /app/test/fixtures/moja/uso_fixture.php

と使い分けられるようにする方法を考えました。

やり方

CakeTestCase を拡張した MyCakeTestCase を作成し、その中で _loadFixtures をオーバーライドします。 なお、参考にしたバージョンは CakePHP 1.3.10 です。

/app/libs/my_cake_test_case.php

<?php
/**
 * Extension of CakeTestCase Class
 *
 * @author  $Author: senge.keiyo $
 * @version $id$
 */
class MyCakeTestCase extends CakeTestCase {

    /**
     * Load fixtures specified in var $fixtures.
     *
     * fixtureディレクトリ配下の任意のディレクトリにフィクスチャを配置できるよう拡張。
     *
     * 例えば $fixtures に 'app.unit_test.uso' と指定すると、
     * /app/tests/fixtures/unit_test/uso_fixture.php をロードできる。
     *
     * @author senda.keijiro
     * @return void
     * @access protected
     */
    function _loadFixtures() {
        if (!isset($this->fixtures) || empty($this->fixtures)) {
            return;
        }

        if (!is_array($this->fixtures)) {
            $this->fixtures = array_map('trim', explode(',', $this->fixtures));
        }

        $this->_fixtures = array();

        foreach ($this->fixtures as $index => $fixture) {
            $fixtureFile = null;
            $fixturePaths = null;

            if (strpos($fixture, 'core.') === 0) {
                $fixture = substr($fixture, strlen('core.'));
                foreach (App::core('cake') as $key => $path) {
                    $fixturePaths[] = $path . 'tests' . DS . 'fixtures';
                }
            } elseif (strpos($fixture, 'app.') === 0) {
                // MODIFIED
                // app.unittest.plan が来たら /fixtures/unittest/plan_fixtures.php
                // をロードするようにする
                $parts = explode('.', $fixture);
                $fixture = $parts[count($parts) - 1];

                array_shift($parts);
                array_pop($parts);
                $path = implode(DS, $parts);

                $fixturePaths = array(
                    TESTS . 'fixtures' . DS . $path,
                    TESTS . 'fixtures',
                    VENDORS . 'tests' . DS . 'fixtures'
                );
            } elseif (strpos($fixture, 'plugin.') === 0) {
                $parts = explode('.', $fixture, 3);
                $pluginName = $parts[1];
                $fixture = $parts[2];
                $fixturePaths = array(
                    App::pluginPath($pluginName) . 'tests' . DS . 'fixtures',
                    TESTS . 'fixtures',
                    VENDORS . 'tests' . DS . 'fixtures'
                );
            } else {
                $fixturePaths = array(
                    TESTS . 'fixtures',
                    VENDORS . 'tests' . DS . 'fixtures',
                    TEST_CAKE_CORE_INCLUDE_PATH . DS . 'cake' . DS . 'tests' . DS . 'fixtures'
                );
            }

            foreach ($fixturePaths as $path) {
                if (is_readable($path . DS . $fixture . '_fixture.php')) {
                    $fixtureFile = $path . DS . $fixture . '_fixture.php';
                    break;
                }
            }

            if (isset($fixtureFile)) {
                require_once($fixtureFile);
                $fixtureClass = Inflector::camelize($fixture) . 'Fixture';
                $this->_fixtures[$this->fixtures[$index]] =& new $fixtureClass($this->db);
                $this->_fixtureClassMap[Inflector::camelize($fixture)] = $this->fixtures[$index];
            }
        }

        if (empty($this->_fixtures)) {
            unset($this->_fixtures);
        }
    }
}
あとはテストケースでこのクラスを呼び出してやればOKです。 **/app/tests/cases/controllers/higes_controller.test.php**
<?php
App::import('Model', 'Uso');
App::import('Lib', 'MyCakeTestCase');

class HigesControllerTestCase extends MyCakeTestCase {
    var $fixtures = array(
        'app.hige.uso'      // /app/test/fixtures/hige/uso_fixture.php がロードされる
    );
?>

副作用

テストケースで「App::import(‘Lib’, ‘MyCakeTestCase’);」すると、なぜかMyCakeTestCaseもテスト対象になるらしく、テスト結果の「n/n test cases complete」の値が1増えます。 テスト結果のGreen/Redには影響しないので、無視してください。

参考

App::import と ClassRegistry::init の違い

このエントリーをはてなブックマークに追加
はてなブックマーク - App::import と ClassRegistry::init の違い
Share on Facebook

命名規則に従わないモデルやプラグインをロードする際、使われるのが「App::import」と「ClassRegistry::init」。どういう使い分けをしているのかわからず、使い方によっては期待通り動かなかったりで困っていたので、違いを調べて見ました。

App::import

API Book によると、下記のような説明があります。

Finds classes based on $name or specific file(s) to search. Calling App::import() will not construct any classes contained in the files. It will only find and require() the file.

「コンストラクトは行わない。ファイルを探してrequire()するだけ」と書いています。

また、戻り値は

boolean true if Class is already in memory or if file is found and loaded, false if not

となっています。

結局このメソッドは「高機能なrequire」と考えて良さそうです。

ClassRegistry::init

これもAPI Bookを参考にすると、以下のように書かれています。

Loads a class, registers the object in the registry and returns instance of the object. ClassRegistry::init() is used as a factory for models, and handle correct injecting of settings, that assist in testing. Return: object instance of ClassName

App::importと異なり、「オブジェクトのインスタンスを作成する」と書いています。

なるほど、テストケースの開始時に ClassRegistry::init() を、終了時に ClassRegistry::flush を行なうのも納得がいきます。テスト毎にモデルのインスタンスを初期化→削除しているんですね。

使い分け

  • 動的にモデルのインスタンスを使う時は ClassRegistry::init
  • それ以外は App::import

という使い分けで良さそうですです。

また検証していないのですが、「CakePHP モデルの読み込みは App::import ではなく ClassRegistry::init で – foldrrの日記」によると、App::importを使うとDBの接続先が $default 固定になってしまうため、ユニットテストで問題が出るそうです。

参考