Tag Archives: UnitTest

スタブとモック

このエントリーをはてなブックマークに追加
はてなブックマーク - スタブとモック
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)で、期待された呼び出しがすべてきちんと行われたかどうかを確認する。

PHPのDBテストに役立つツール: Phactory

このエントリーをはてなブックマークに追加
はてなブックマーク - PHPのDBテストに役立つツール: Phactory
Share on Facebook

Phactory

Phactory – A Database Factory for PHP Unit Tests

RoRにヒントを得たPHPのDBテストツールです。DBに簡単にテストデータの出し入れができるので、DB関連のテスト時に威力を発揮します。

  • O/Rマッパーを利用してオブジェクト指向的にDBとのデータ交換が可能
  • テストデータをテストコードに埋め込めることができる
  • ブループリント(blueprints)により、ダミーデータや連番のテストデータが簡単に作成できる

PHPUnit拡張のDBTestは、別ファイルでフィクスチャを用意しなければならなかったり、おまじないコードが長すぎて手動では絶対に書けないものだったりし、非常に使い勝手が悪いです。Phactoryはこうした不満の解消に役立ちそうです。

まだバージョンは 0.1.0 ということですが、これからの発展が期待されます。

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のテストを見てやっと気付きました。

参考

翻訳: Unit Testing CakePHP Shells

このエントリーをはてなブックマークに追加
はてなブックマーク - 翻訳: Unit Testing CakePHP Shells
Share on Facebook

Unit Testing CakePHP Shells | Mark Story

シェルの単体テストをどうやっていいかわからないなら、上の記事はなかなかおすすめです。テストしたいクラスとShellDispatcherをモック化する方法が紹介されています。

サンプルコードは、CakePHPに組み込まれているCoreテストの「/cake/tests/cases/console/libs/shell.test.php」にほぼ同じものがあります。筆者のMark Story氏も言っていますが、具体的な例が見たければそちらのソースコードを参照するといいでしょう。

以下、翻訳です。


シェルは単体テストをするのが難しいものの一つです。WEBコンテキストと違いCLIコンテキストの中で動作するため、うまくテストするのはちょっとしたチャレンジになるでしょう。特に高いハードルとなるのは、CLI(コマンドライン環境)とシェルを分離することと、正しい引数・パラメータをシミュレーションすることです。

環境依存要素の除去

シェルとCLIを分離するには、モックオブジェクトを使います。まずはテスト対象のシェルとShellDispatcherをモック化しましょう。ShellDispatcherをモッキングするのは特に重要です。以下、自作のクラス「AclExtrasShell」をテストする場合を例に説明します。シェルのテストでは、CLI環境と連動しているメソッドをモッキングする必要があります。

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', 'AclExtrasMockShellDispatcher',
    array('getInput', 'stdout', 'stderr', '_stop', '_initEnvironment')
);

入出力メソッドと _stop メソッドをモッキングすることで、ShellDispatcherからstdin(標準入力)とstdout(標準出力)を分離しました。また、テストケースがCLIコンテキストから独立して動作するようにしました。続いて、実際にテストしたいクラス内のいくつかのメソッドをモック化しましょう。

Mock::generatePartial(
    'AclExtrasShell', 'MockAclExtrasShell',
    array('in', 'hr', 'out', 'err', 'createFile', '_stop', 'getControllerList')
);

繰り返しますが、入力と出力に関わるメソッドはとても重要です。_stopもモック化することで、シェルが終了しようとしてもテストが継続できるようにします。これら二つのクラスをメソッドをモック化したら、テストメソッドの準備に取りかかりましょう。

テストケースの準備

シェルのテストと普通のテストケースは、なんら変わりません。ただしシェルの場合は少し違ったstartTestとendTestを必要とします。私はふだんは以下のようにします。

class AclExtrasShellTestCase extends CakeTestCase {
//...
function startTest() {
    $this->Dispatcher =& new AclExtrasMockShellDispatcher();
    $this->Task =& new MockAclExtrasShell($this->Dispatcher);
    $this->Task->Dispatch =& $this->Dispatcher;
    $this->Task->Dispatch->shellPaths = Configure::read('shellPaths');
}
//...
function endTest() {
    unset($this->Task, $this->Dispatcher);
    ClassRegistry::flush();
}

startTestの中で、モック化したShellDispatcherとテスト対象のシェルのインスタンスを作成します(4-5行目)。続いて手動でDispatcherとシェルをDispatchプロパティにアサインします(6行目)。さらにshellPathsを設定することで、オブジェクトとシェルのシミュレーションが可能になりました。endTestメソッドでは後掃除をし、テストの度に新しいオブジェクトを使えるようにします。注意として、もしシェル内でほかのシェルを利用している場合は、そちらもモック化などの準備をする必要があります。全部のDispatchの設定をするのもお忘れ無く。

in()とout()をアサートする

in()とout()はモック化されているので、代わりにsetReturnValue()とsetReturnValueAt()を使ってユーザーからの入力をシミュレートします。出力のアサーションにはexpectAt()を使います。例えば:

$this->Task->expectOnce('out');
$this->Task->expectAt(0, 'out', array(new PatternExpectation('/recovered/')));

「/recovered/」というパターンが一度出力されるであろう部分で、上記のように予想をセットします。呼び出される回数と個別に呼び出される部分を一緒に書くとうまくいくようです。個別に呼び出される部分だけ書いても、メソッドが呼ばれなかった場合はテストはfailになります。コール回数の予想だけだと、メソッドが呼ばれても何が起こっているのか知ることはできません。

さらに複雑な例

上記の通りやればうまくいきますが、もっと複雑なシェルのやりとりはどうすればいいでしょうか?そういう時はより細かいメソッドに分割すると良いでしょう。分割できないなら、より多くの手間を費やしてin()とout()の予想を書くほかないです。例として、ViewTaskのテストの一部分を取り上げましょう(bakeの一部分です)。

$this->Task->connection = 'test_suite';
$this->Task->args = array();
 
$this->Task->Controller->setReturnValue('getName', 'ViewTaskComments');
$this->Task->Project->setReturnValue('getAdmin', 'admin_');
$this->Task->setReturnValueAt(0, 'in', 'y');
$this->Task->setReturnValueAt(1, 'in', 'n');
$this->Task->setReturnValueAt(2, 'in', 'y');
 
$this->Task->expectCallCount('createFile', 4);
$this->Task->expectAt(0, 'createFile', array(
    TMP . 'view_task_comments' . DS . 'admin_index.ctp',
    new PatternExpectation('/ViewTaskComment/')
));
$this->Task->expectAt(1, 'createFile', array(
    TMP . 'view_task_comments' . DS . 'admin_view.ctp',
    new PatternExpectation('/ViewTaskComment/')
));
$this->Task->expectAt(2, 'createFile', array(
    TMP . 'view_task_comments' . DS . 'admin_add.ctp',
    new PatternExpectation('/Add ViewTaskComment/')
));
$this->Task->expectAt(3, 'createFile', array(
    TMP . 'view_task_comments' . DS . 'admin_edit.ctp',
    new PatternExpectation('/Edit ViewTaskComment/')
));
 
$this->Task->execute();

上記のテストでは複数のモックを使っています。また、どうやってシェルメソッドの予想とアサーションを書くかを示しています。PatternExpectationsをどう使うかと同様、モック化されたメソッドの入力をどうアサートするかわかるでしょう。

シェルクラスのテストが少しは楽になりましたか?もっと多くの例が知りたければ、coreのシェルクラスのテストケースを見るといいでしょう。

コントローラを単純化するために例外を使う

このエントリーをはてなブックマークに追加
はてなブックマーク - コントローラを単純化するために例外を使う
Share on Facebook

2009年6月12日の記事でだいぶ古いのですが、CakePHPのコードを良くするTIPSがあったので訳してみました。

Simplifying Controller logic with Exceptions | Mark Story

要約すると、「モデル内で例外を吐くようにすると、エラーコードが読みやすくなり、コントローラのテストもしやすくなるよ」ということです。


増大するコードと格闘する日々・・・どうにかクリエイティブな方法で解決したい。そんな悩みを、モデルのメソッドから例外を投げる方法で解決しました。別にビックリするようなものではないです。ただfalseを返すより、ちょっと便利な点がいくつかあります。

第一に、if-elseを減らすことができます。第二に、エラーが起こる部分のソースにエラーメッセージを書くことができるので、何度も使うメソッドならエラーメッセージを重複させずに書くことができます。

例えば以下のメソッドは、リモートアドレスからリソースをダウンロードし、 ローカルファイルシステムとデータベースに記録するものです。(訳注: ダウンロードの時点で失敗すると例外を、保存に成功するとtrueを、保存に失敗するとfalseを返します。)

public function downloadResource($url, $userId, $type = 'image') {
    if (!isset($this->_fetchMimes[$type])) {
        throw new OutOfBoundsException(__('Invalid media type', true));
    }
    $this->_loadSocket($url);
    $resource = $this->Socket->get($url);
    if (!isset($this->Socket->response['header']['Content-Type'])) {
        throw new OutOfBoundsException(__('Submitted url has no Mime-Type', true));
    }
    $allowedContentTypes = $this->_fetchMimes[$type];
    if (!in_array($this->Socket->response['header']['Content-Type'], $allowedContentTypes)) {
        throw new OutOfBoundsException(__('Submitted url has an invalid Mime-Type', true));
    }
    $newFile = array(
        'File' => array(
            'file' => $this->_saveFetchedFile($resource, $url, $this->Socket->response['header']['Content-Type']),
            'user_id' => $userId,
            'title' => $url,
        )
    );
    $this->create($newMedia);
    if ($this->save()) {
        return true;
    }
    return false;
}

見てわかる通り、起きてはならないことが起きた時には OutOfBoundsExceptions を投げています。最近のバージョンのPHPに含まれているSPLライブラリ (訳注: 標準PHPライブラリ) には、便利なクラスが多数用意されています。もちろん自分で例外を作ることもできますが、たいていは組み込みの例外を使うだけで十分でしょう。

これを使えば、コントローラのメソッドをかなりスッキリさせることができます。何重ものifでチェックする必要が無くなります。また、シンプルでエラーの見通しも良いコードを書くことができます。

try {
    $this->File->downloadResource($this->data['File']['url'], $this->Auth->user('id'), 'image');
    //do some additional file handling and data processing.
 
    $this->Session->setFlash(__('File uploaded successfully', true));
} catch(OutOfBoundsException $e) {
    $this->Session->setFlash($e->getMessage());
}
$this->redirect(array('action' => 'index'));

このメソッドの例外発生をテストするのは簡単です。assertFalse を使うのではなく、pass()とfail()を使えばいいのです。

// (訳注) 不正なURLを送った時に例外が発生することをテストする
try {
    $this->File->downloadResource('http:/bogus.com/', 1, 'image');  // 間違ったURL
    $this->fail('No exception thrown with bogus arguments');
} catch (Exception $e) {
    $this->pass('Exception thrown');
}

例外を使うと便利になることは多いです。だからと言って、falseを返すタイプのメソッドすべてを例外を返すようにする必要はないでしょう。たとえばヘルパから例外を返すようにすると、要望を満たしてくれるよりも苦痛を感じることのほうが多くなるでしょう。

どんなツールもそうですが、正しく使えばメンテナンス性の良いコードを生み出すことができるのです。

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) が呼び出されるようです。

参考