Category Archives: SimpleTest

スタブとモック

このエントリーをはてなブックマークに追加
はてなブックマーク - スタブとモック
Share on Facebook

今までモックをほとんど使ったことがなかったので、勉強してみました。

スタブとは

refs: スタブ – Wikipedia

呼び出す側(上位)のモジュールを検査する場合に、呼び出される側(下位)の部品モジュールが未完成であることがある。このとき、呼び出される側の部品モジュールの代用とする仮のモジュールを、「スタブ」と呼ぶ。スタブモジュールは設計仕様に定義されている全ての関数を実装してあるが、関数内部は正規の動作をする事無く適当な定数を返すというような作りになっている事が多い。

「必ずfalseを返すスタブ」「ランダムな整数を返すスタブ」なんて言葉はよく聞きます。

モックとは

refs: スタブとモックの違い – ソフト開発お仕事メモ

モック戦略を使用する際には、3つの手順を踏むことになります。

No 手順名 説明
1 期待値の設定 モックオブジェクトに対して、メソッドが呼び出されるべき順序を記録します。その際に、期待される引数、モックオブジェクトのメソッドが返す戻り値も設定します。
2 テスト条件下でのモックオブジェクトの使用 モックオブジェクトを使用したテストを実施します。
3 結果の検証 モックオブジェクトに対し、期待されたとおりにモックオブジェクトが使用されたか問い合わせます。

そもそもテストの条件とは?

refs: Ruby on Rails でのモックとスタブの作成

  • 反復可能なこと。テストを自動化する場合には、テスト結果を検証できるように、テスト・ケースは毎回同じ結果を返す必要があります。
  • 実行時間が短いこと。テスト・ケースの実行に時間がかかりすぎるようだと、テスト・ケースを実行しなくなるのでテスト・ケースが役に立ちません。
  • 単純なこと。テストの作成が難しすぎると、テストを作成しなくなります。

スタブやモックが必要になる理由

refs: Start! Ruby – RSpecの構文

  • 全てを「本物」でテストしようとすると、「全てが揃わないとテストできない」という本末転倒な事が起こりかねない。
  • たとえば時刻に関するオブジェクトのように、システムの構成によって変化してしまうオブジェクトがあると、テスト環境によって差異ができてしまう。
  • UnitTestが大きな問題に移ると段々と結合テスト化してしまう、という問題がある。

※ ただし、スタブ/モックを多用し過ぎると、今度はインタフェース不一致の発見を先送りにする、という状況にもなりかねない。このあたりはさじ加減が必要。

モックでできること

refs: PHPUnit3で始めるユニットテスト:第4回 モックオブジェクトを使ったテスト|gihyo.jp … 技術評論社

  • 生成されるオブジェクトにメソッドを定義する
  • そのメソッドの振る舞いを指定する
    • 実行回数の制約を設ける
      • たとえば「1回のみ呼び出される」や「0回以上呼び出される」
    • メソッド名を指定する
    • 具体的な振る舞いを記述する
      • メソッドの戻り値
      • メソッドが投げる例外

スタブとモックの違い

refs: Ruby on Rails でのモックとスタブの作成

モックとスタブの違い – [lib]

モック・オブジェクトは一種のスタブです。モック・オブジェクトは、テスト対象のオブジェクトを使用するクライアント・コードを置き換えます。しかしモック・オブジェクトはそれ以上のことを行い、テスト対象のオブジェクトがクライアント・コードを実際にどう使うかを測定するのです。

インターフェースの使い方をテストする場合にはモックを、インターフェースの使い方をまったく気にしない場合にはスタブを使う必要があります。

モック・オブジェクトの作成は、スタブの作成とよく似ています。違いは、スタブは受動的であるということです。スタブは、スタブの作成対象のメソッドに対して呼び出しを行う実在のソリューションを単にシミュレーションするにすぎません。一方モックは能動的であり、モック・オブジェクトを使って行うその方法を実際にテストします。想定の動作と一致する方法でモックを使わないと、テストは失敗します。

refs: Martin Fowler’s Bliki in Japanese – テストダブル

スタブは、テスト時の呼び出しに対して、あらかじめ用意された結果を返す。通常、テスト用にプログラムされたところ以外には応答しない。スタブは呼び出しの情報を記録することもある。例えば、Eメールゲートウェイスタブは「送られた」メッセージを記録するような場合だ。単に「送られた」メールの数を記録する場合もあるだろう。

モックは、エクスペクテーションが事前にプログラムされたものである。エクスペクテーションとは、受信する一連の呼び出しの仕様を表わしたものである。期待されない呼び出しが行なわれた場合は例外をスローする。また、テスト実行後の検証(verification)で、期待された呼び出しがすべてきちんと行われたかどうかを確認する。

CakePHPシェルの単体テストのやり方

このエントリーをはてなブックマークに追加
はてなブックマーク - CakePHPシェルの単体テストのやり方
Share on Facebook

CakePHPのシェルの単体テストは少々面倒くさいです。WEB表示を前提としたコントローラやコンポーネントと違ってCLIへの出力をするので、CakePHP内のリクエスト順が違います。そのためDispatcherの設定などを変えなければなりません。

ただ、深く考えずにシェルのテストをしたいだけなら、Coreのテストからソースをコピーするだけで動きます。

以下に簡単なサンプルを載せます。

サンプル

テスト対象のシェル: uso.php

// app/vendors/shells/uso.php
<?php
class UsoShell extends Shell {

    function main() {
        $this->out("uhouho");
    }
}
?>

テストケース: uso.test.php

// app/tests/cases/shells/uso.test.php
<?php
App::import("Shell", "Uso");

if (!defined('DISABLE_AUTO_DISPATCH')) {
    define('DISABLE_AUTO_DISPATCH', true);
}

if (!class_exists('ShellDispatcher')) {
    ob_start();
    $argv = false;
    require CAKE . 'console' .  DS . 'cake.php';
    ob_end_clean();
}

Mock::generatePartial('ShellDispatcher', 'UsoShellMockShellDispatcher', array(
    'getInput', 'stdout', 'stderr', '_stop', '_initEnvironment'
));

class UsoShellTestCase extends CakeTestCase {
    
    function setUp() {
        $this->Dispatcher =& new UsoShellMockShellDispatcher();
        $this->Shell =& new UsoShell($this->Dispatcher);
        $this->Shell->Dispatch = $this->Dispatcher;
        $this->Shell->Dispatch->shellPaths = Configure::read("shellPaths");
    }
    
    function tearDown() {
        unset($this->Shell, $this->Dispatcher);
        ClassRegistry::flush();
    }
    
    // テストメソッド
    function testMain() {
        $this->Shell->Dispatch->expectAt(0, "stdout", array("uhouho\n", false));
        $this->Shell->main();
    }
}
?>

実行方法と出力結果

$ cd c:/xampp/htdocs/cakephp/app
$ ../cake/console/cake testsuite app case shells/uso


Welcome to CakePHP v1.3.2 Console
---------------------------------------------------------------
App : app
Path: c:\xampp\htdocs\cakephp\app
---------------------------------------------------------------
CakePHP Test Shell
---------------------------------------------------------------
Running app case shells/uso
1/1 test cases complete: 1 passes.
Time taken by tests (in seconds): 0.022739887237549
Peak memory use: (in bytes): 12,622,648

解説

uso.test.phpの先頭部分で何をやっているのかわかりづらいと思います。要はモックオブジェクトを使って、Dispatcherの書き換えと、CLIへの入出力の無効化をしています。あとはそのモックをsetUp()内で読み込んでいます。詳しいことは下記の「参考」にあるアドレスを見てください。

また、「expectAt()」というメソッドはSimpleTestのアサーションの一つです。詳しいことはSimpleTest for PHP mock objects documentationを参考にしてください。

地味な注意ですが、expectAt()で予期している文字列は “uhouho” ではなく “uhouho\n“です。$this->out()は改行記号付きで標準出力するメソッドです。何回やってもfailになるのでおかしいと思い、Coreのテストを見てやっと気付きました。

参考

モデルを使わないコンポーネントの単体テスト

このエントリーをはてなブックマークに追加
はてなブックマーク - モデルを使わないコンポーネントの単体テスト
Share on Facebook

CakePHP 1.3.2でコンポーネントを単体テストする場合、モデルの有無によって大幅に面倒くささが変わってきます。なぜならコンポーネントはモデルを直接操作できないので、間にコントローラを作成する必要があるからです。

公式マニュアルにある方法はモデルを使うケースのみで、しかもわかりづらいです (どのファイルのどのブロックに書いてるのかわからない)。今回はモデル無しのコンポーネントのテスト方法をメモしておきます。

やり方

以下のコンポーネント、「HigeComponent」をテストする場合を考えます。

// app/controllers/components/hige.php
<?php
class HigeComponent extends Object {
    function moja() {
        return "mojamoja";
    }
}
?>

テストケースは下記の通り、コンポーネントを読み込んで直接実行するだけです。

// app/tests/cases/components/hige.test.php
<?php
App::import("Component", "Hige");
    
class HigeComponentTestCase extends CakeTestCase {
    function setUp() {
        $this->component = new HigeComponent();
    }
    
    // テストケース
    function testMoja() {
        $result = $this->component->moja();
        $expected = "moja";
        
        $this->assertEqual($result, $expected);
    }
}
?>

あとは、下記の通りコマンドを打ち込めばOK。(cake.batにパスを通さずやってます)

cd c:\xampp\htdocs\cakephp\app
../cake/console/cake testsuite app case components/hige

出力結果


Welcome to CakePHP v1.3.2 Console
---------------------------------------------------------------
App : app
Path: c:\xampp\htdocs\cakephp\app
---------------------------------------------------------------
CakePHP Test Shell
---------------------------------------------------------------
Running app case components/hige
1/1 test cases complete: 1 passes.
Time taken by tests (in seconds): 0.016170024871826
Peak memory use: (in bytes): 11,534,288

参考

assertionは下記を参考に。

コントローラからビューに渡る変数をアサーションする

このエントリーをはてなブックマークに追加
はてなブックマーク - コントローラからビューに渡る変数をアサーションする
Share on Facebook

公式マニュアルの「コントローラのテスト」では、ただ単に変数をdebugして吐いてるだけで一切アサーションをしていません。これってテストと言えるのでしょうか?コントローラのメソッドが実行された後に予期した値が入っているかチェックするにはどうすればいいでしょうか?

調べたところ、コントローラ内でsetされた変数は viewVars というメンバ変数に格納されるようです。例えば、下記のようなコントローラがあった場合。

// app/controllers/usos_controller.php
class UsosController extends AppController {

    var $name = 'Usos';

    function hige() {
        $this->set('moja', $this->Uso->find("all"));
    }
}
?>

これをコントローラのテストクラスで取得するには下記のようにすれば良いです。

// app/tests/cases/controllers/usos_controller.test.php
<?php
/* Usos Test cases generated on: 2010-07-16 16:07:53 : 1279264793*/
App::import('Controller', 'Usos');

class TestUsosController extends UsosController {
    var $autoRender = false;
 
    function redirect($url, $status = null, $exit = true) {
        $this->redirectUrl = $url;
    }
 
    function render($action = null, $layout = null, $file = null) {
        $this->renderedAction = $action;
    }
 
    function _stop($status = 0) {
        $this->stopped = $status;
    }
}


class UsosControllerTestCase extends CakeTestCase {
    var $fixtures = array('app.uso');

    function startTest() {
        $this->Usos =& new TestUsosController();
        $this->Usos->constructClasses();
    }

    function endTest() {
        unset($this->Usos);
        ClassRegistry::flush();
    }

    function testHige() {
        $this->Usos->hige();
        $result = $this->Usos->viewVars;
        var_dump($result);
    }
}
?>

出力結果

array(1) {
  ["moja"]=>
  array(1) {
    [0]=>
    array(1) {
      ["Uso"]=>
      array(4) {
        ["id"]=>
        string(1) "1"
        ["name"]=>
        string(11) "Miles Davis"
        ["created"]=>
        string(19) "2010-07-16 15:00:49"
        ["modified"]=>
        string(19) "2010-07-16 15:00:49"
      }
    }
  }
}

参考

CakePHP 1.3で単体テスト

このエントリーをはてなブックマークに追加
はてなブックマーク - CakePHP 1.3で単体テスト
Share on Facebook

やり方

bake test でテストケース&フィクスチャを作成する場合

cake bake test
cake bake fixture 

app/tests/ 以下に自分でテストケースを書く場合

  1. テスト用データベース定義を app/config/database.php に書く
  2. bakeコマンドでテストケースとフィクスチャの雛形を作成する
  3. 実行する
# コントローラーのusos_controllerをテストする場合
cake testsuite app case controllers/usos_controller 

# コンポーネントのusos_commonをテストする場合
cake testsuite app case components/usos_common

redirectやexitを使うメソッドをテストする時の注意

テスト中にredirectメソッドやexitsメソッドを踏むと、ただちにテストが終了してしまいます。下記URLを参考にして、独自のtestsuite.phpとcontroller.phpを用意する方法でうまくいきました。記事ではCakePHP 1.2.7を使っているようですが、CakePHP 1.3.2でもうまく動きました。

テスト時のredirect()やcakeError() の結果を拾う。+ DebugKit使用時も。 – ziegler(チーグラー) ver.6.0

公式マニュアル にある方法 (something_controller.test.php と /cake/tests/lib/cake_test_case.php を修正する) ではうまく動きませんでした。結局redirectの瞬間に終了してしまいました。何が間違っているのか。

[cakephp]testActionでredirectするアクションをテストする方法 | Ryuzee.com を参考にPHP拡張のrunkitを使うのも検討しましたが、どう探してもこのライブラリをWindowsで動かす方法が見つかりませんでした。ソースからコンパイルする方法もあるようですが、最新版でもないVisualStudioをインストールしなければならないなど面倒なようだったので、やめました。

それにしても、基本的な単体テストの準備をするのに手間がかかりすぎです。CakePHP 2.0ではPHPUnitが採用されるようなので、早くリリースしていただきたいところ。

参考

CakePHP 単体テストで使うメソッドのコールバックの実行順

このエントリーをはてなブックマークに追加
はてなブックマーク - CakePHP 単体テストで使うメソッドのコールバックの実行順
Share on Facebook

公式マニュアルだけではSimpleTestのコールバックの実行される順序がわからなかったので、調べた結果をメモしておきます。

下のようなコードを実行してみました。実際のテストメソッドは「testHige()」です。

<?php
App::import('Controller', 'Usos');

class TestUsosController extends UsosController {
    var $autoRender = false;

    function redirect($url, $status = null, $exit = true) {
        $this->redirectUrl = $url;
    }
}

class UsosControllerTestCase extends CakeTestCase {
    var $fixtures = array('app.uso');

    function startTest() {
        echo("startTest\n");
        $this->Usos =& new TestUsosController();
        $this->Usos->constructClasses();
    }

    function endTest() {
        echo("endTest\n");
        unset($this->Usos);
        ClassRegistry::flush();
    }
    
    function start() {
        echo("start\n");
    }
    
    function end() {
        echo("end\n");
    }
    
    function startCase() {
        echo("startCase\n");
    }
    
    function endCase() {
        echo("endCase\n");
    }
    
    function before($method) {
        echo($method . " before\n");
    }
    
    function after($method) {
        echo($method . " after\n");
    }

    // 実際のテストメソッド
    function testHige() {
        echo("testHige\n");
    }
}
?>

実行結果は以下の通りです。

start before
start
start after
startCase before
startCase
startCase after
testHige before
testHige
testHige after
endCase before
endCase
endCase after
end before
end
end after

大まかに書くと「start() → startCase() → testHige() → endCase() → end()」の順に実施され、それぞれのメソッドの前後にbefore($method) と after($method) が呼び出されるようです。

参考