Home > アーカイブ > 2006-11

2006-11

PHPUnitでMockオブジェクトを使う2

この記事の所要時間: 338

PHPUnitでMockオブジェクトを使うで紹介したMockオブジェクトですが、これは元クラスを継承しているわけではないので当然ながら元クラスの実装は使えません。

ただテストする場面によっては、基本は元クラスの振る舞いをして、一部のメソッドだけMockにしたいという場合があります。

そこで元クラスを継承したMockオブジェクトを作ってみます。

元クラス

PHP:
  1. <?php
  2. class Hoge {
  3.   // このメソッドはそのまま
  4.   function execute() {
  5.     if ($this->request()) {
  6.       // OK
  7.       return 1;
  8.     } else {
  9.       // NG
  10.       return -1;
  11.     }
  12.   }
  13.  
  14.   // このメソッドをMockにしたい
  15.   function request(&$db) {
  16.     $db->query();
  17.     sleep(100);
  18.     return true;
  19.   }
  20. }
  21. ?>

テストケース

Mockオブジェクトをテストケースに書いています。単にHogeクラスを継承してrequest()をオーバーライドするだけです。

PHP:
  1. <?php
  2. require_once 'PHPUnit.php';
  3. require_once 'Hoge.php';
  4.  
  5. class MockHoge extends Hoge {
  6.   function request() {
  7.     return true;
  8.   }
  9. }
  10.  
  11. class HogeTest extends PHPUnit_TestCase {
  12.   function testExecute() {
  13.     $obj = new MockHoge();
  14.     $this->assertTrue($obj->execute());
  15.   }
  16. }
  17.  
  18. $suite = new PHPUnit_TestSuite('HogeTest');
  19. $result = PHPUnit::run($suite);
  20. echo $result->toString();
  21. ?>

テストケース-返り値を設定

こちらではMockHogeに変数を追加して、インスタンス変数に応じてrequest()メソッドの返り値が変わるようにしています。

PHP:
  1. <?php
  2. require_once 'PHPUnit.php';
  3. require_once 'Hoge.php';
  4.  
  5. class MockHoge extends Hoge {
  6.   var $mockRequestValue;
  7.  
  8.   function request() {
  9.     return $this->mockRequestValue;
  10.   }
  11. }
  12.  
  13. class HogeTest extends PHPUnit_TestCase {
  14.   function testExecute() {
  15.     $obj = new MockHoge();
  16.  
  17.     $obj->mockRequestValue = true;
  18.     $this->assertEquals(1, $obj->execute());
  19.     $obj->mockRequestValue = false;
  20.     $this->assertEquals(-1, $obj->execute());
  21.   }
  22. }
  23.  
  24. $suite = new PHPUnit_TestSuite('HogeTest');
  25. $result = PHPUnit::run($suite);
  26. echo $result->toString();
  27. ?>

この方法では(Mockオブジェクト用の)コンポーネントは必要ないので手軽にMockオブジェクトを使う事ができます。また元クラスの実装を生かせるのでこの例のように一部だけを変えたい場合には有用です。

SimpleTestのようなMockオブジェクトとケースバイケースで使い分けるのが良いですね。

エンジニアのための時間管理術

この記事の所要時間: 123
エンジニアのための時間管理術
Thomas A. Limoncelli 株式会社クイープ
オライリー・ジャパン
売り上げランキング: 973

コンピュータエンジニア向けのLifeHack本です。

仕事術としてタイムマネジメントが紹介されているのですが

  • タスクリストは全て外部記憶に追い出す(脳はより生産的な事に使う)
  • 大きなタスクは実現しやすい細かいタスクに分ける
  • 仕事とプライベートのタスクを同じように管理する

など、GTDに通じるところがあります。

さらにこの本独自の

  • 自動化できるものは自動化してしまう
  • 目的に応じたドキュメントを作る
  • 日々のタスクと中長期的なタスクの折り合い
  • タイムマネジメントと顧客対応の両立

といったより実践的な内容もあります。

エンジニア向けと銘打っているだけあって自動化のところではUnixコマンドが紹介されるなど、この仕事をしている人ならではのTipsが参考になりました。仕事術系の本でawkの使い方を解説しているのは見たことないです。;-)

私の場合、GTDで躓いたのが「やりたいリスト」を出した後、それらを上手く分類・実践していくことでした。(実際今もリストに埋もれているままだったりしてます。。。)

そこでこの本のタイムマネジメントの方法(日々の始めに実施するタスクを決める、優先順位を付ける等)を組み合わせると上手くいくではないかと考えています。

「Just do it」の精神で早速実践ですね。

PHPUnitでMockオブジェクトを使う

この記事の所要時間: 347

PHPUnit3がリリースされました。このバージョンにはMockがサポートされているのですが、PHPUnit3はPHP5を対象としているので残念ながらPHP4では動作しません。ですのでPHP4対応のPHPUnitでMockクラスを使う方法を探ってみました。

使うのはPHPUnitと並ぶユニットテストツールのSimpleTestです。こちらはPHP4対応でMockクラスがサポートされています。これをPHPUnitと組み合わせて使ってみます。

SimpleTestインストール

PEARパッケージが以前はあったようなのですが、sourceforgeにはそれらしいものが見当たりませんでした。
zuzara : symfonyチュートリアル実践【第14~16日目】を参考に以下のコマンドでインストールしました。

CODE:
  1. $ pear install http://jaist.dl.sourceforge.net/sourceforge/simpletest/simpletest_1.0.0.tgz

PEARパッケージが見つからない場合でもsourceforgeからソースを取得してinclude_pathで参照できる位置に配置すればOKです。

PHPUnit用Mockクラス

SimpleTestのSimpleMockを継承してPHPUnitで使うMockクラスを記述します。

PHP:
  1. <?php
  2. require_once 'simpletest/mock_objects.php';
  3.  
  4. class PHPUnitMock extends SimpleMock {
  5.   function PHPUnitMock($test, $wildcard) {
  6.     $this->SimpleMock($test, $wildcard);
  7.   }
  8. }
  9. SimpleTestOptions::setMockBaseClass('PHPUnitMock');
  10. ?>

テスト対象クラス

テスト対象のクラスはこちらです。selectCount()はテーブル名を引数に取り、テーブルのレコード数を整数で返します。擬似的に処理時間が100秒かかるようにしています。

PHP:
  1. <?php
  2. class Database {
  3.   function selectCount($table) {
  4.     sleep(100);
  5.     return null;
  6.   }
  7. }
  8. ?>

テストケース

Databaseクラスのテストケースを記述します。Mockクラスを使用することに定義した処理結果[100]がすぐに返ってきます。

PHP:
  1. <?php
  2. require_once 'PHPUnit.php';
  3. require_once 'PHPUnitMock.php';
  4. require_once 'Database.php';
  5.  
  6. Mock::generate('Database');
  7.  
  8. class DatabaseTest extends PHPUnit_TestCase {
  9.   function DatabaseTest ($name) {
  10.     $this->PHPUnit_TestCase($name);
  11.   }
  12.  
  13.   function setup() {
  14.   }
  15.  
  16.   function testQuery() {
  17.     $mock =& new MockDatabase($this);
  18.     $mock->setReturnValue('selectCount', 100, 'users');
  19.  
  20.     $ret = $mock->selectCount('users');
  21.     $this->assertEquals($ret, 100);
  22.   }
  23. }
  24.  
  25. $suite = new PHPUnit_TestSuite('DatabaseTest');
  26. $result = PHPUnit::run($suite);
  27. echo $result->toString();
  28. ?>

SimpleTestはMockクラスの他にHTTPテストも行えるなど高機能なのですが、すでにPHPUnitを使用していたので今更移行するのも億劫でした。あとはコードカバレッジも使えると良いのですが。;-)

foreachでIndirect modification of overloaded propertyが発生する

この記事の所要時間: 222

マジックメソッドの__get()ですが、配列を返すとforeachでNoticeが発生する場合があります。

PHP:
  1. <?php
  2.  
  3. class A {
  4.   private $array = array(1, 2, 3);
  5.  
  6.   function __get($name) {
  7.     if ($name == 'hoge') {
  8.       return $this->array;
  9.     }
  10.   }
  11. }
  12.  
  13. $obj = new A();
  14. foreach ($obj->hoge as $elem) {
  15.   var_dump($elem);
  16. }
  17. ?>
PHP Notice:  Indirect modification of overloaded property A::$hoge has
 no effect in __get.php on line 15

The reason for this is that __get() only returns variables in read mode, while foreach() wants a variable in read/write mode as it tries to modify the internal array pointer. As it can't do this PHP 5.2 will now throw a warning on this.

Overloaded properties (__get) - Derick Rethans

理由はforeachではread/writeモードが要求されるのに__get()ではreadモードの値が返されるためのようです。__get()がreadモードで返すのは当然のような気がしますがちと不便ですね。

対策としては__get()内で(array)でキャストするか

PHP:
  1. <?php
  2.   function __get($name) {
  3.     if ($name == 'hoge') {
  4.       return (array)$this->array;
  5.     }
  6.   }
  7. ?>

__get()の値を別の変数に代入すれば良いようです。

PHP:
  1. <?php
  2. $array = $obj->hoge
  3. foreach ($array as $elem) {
  4. ?>

この現象は5.2.0で発生し、5.1.6ではNoticeはでませんでした。__get()は便利で何だか使いたくなる機能ですが、配列を返す場合は注意しましょう。

前の日曜日は何日?

この記事の所要時間: 057

strtotime()で様々な日付をunixタイムスタンプで取得できます。

Keep it simple and just do strtotime('-1 Sunday',$currentDay);

Calculating start and end dates of a week. - Derick Rethans (2006-11-18)

コメント欄からの引用です。

エントリにある週の始め、終わりの日にちを取得するには以下で取得できます。

PHP:
  1. <?php
  2. $now = time();
  3. // 週の始め(月曜日)
  4. echo date('Y/m/d', strtotime('-1 Monday', $now)) . "\n";
  5. // 週の終わり(日曜日)
  6. echo date('Y/m/d', strtotime('+1 Sunday', $now)) . "\n";
  7. ?>

PHPマニュアルに他の記述も載っていますのでご参考にどうぞ。

■参考サイト
PHPマニュアル-strtotime

ユニットテストでデータ検証

この記事の所要時間: 35

ユニットテストでプログラムで生成したデータを検証する方法です。

CSVファイルを検証するテストをPHPUnitで書いてみました。

PHP:
  1. <?php
  2. require_once "PHPUnit.php";
  3.  
  4. class DataTest extends PHPUnit_TestCase {
  5.   function DataTest($name) {
  6.     $this->PHPUnit_TestCase($name);
  7.   }
  8.  
  9.   function testData() {
  10.     // データ読み込み
  11.     $fp = fopen('data.csv', 'r');
  12.     if (!$fp) {
  13.       die('data not found.');
  14.     }
  15.  
  16.     // テストループ
  17.     $line = 0;
  18.     while(!feof($fp)) {
  19.       $line++;
  20.       $row = fgetcsv($fp, 4096);
  21.       if (empty($row)) {
  22.         continue;
  23.       }
  24.  
  25.       if (count($row) != 3) {
  26.         $this->fail('Line: ' . $line);
  27.         continue;
  28.       }
  29.  
  30.    // id は数値のみ
  31.       $id = $row[0];
  32.       $this->assertRegExp('/^[0-9]+$/', $id, $line);
  33.  
  34.    // code は英数字のみ
  35.       $code = $row[1];
  36.       $this->assertRegExp('/^[A-Za-z0-9]+$/', $code, $line);
  37.  
  38.    // name は値があれば良い
  39.       $name = $row[2];
  40.       $this->assertTrue(strlen($code)> 0, $line);
  41.     }
  42.  
  43.     fclose($fp);
  44.   }
  45. }
  46.  
  47.  
  48. $suite = new PHPUnit_TestSuite('DataTest');
  49. $result = PHPUnit::run($suite);
  50. echo $result->toString();
  51. ?>

ポイントは「assert失敗時のメッセージにデータを特定する項目を含めておく」くらいでしょうか。(このソースでは行数を入れていますが、DBのRecordsetを検証する時はpkeyの値を入れています。)あとはdata.csvが何万行あろうが何回テストしようがphpunitがきっちりテストしてくれます。やっている事は単純なのですが効果は高いです。

他にも複雑なSQLで生成されるデータをPHPで取得して検証したり、DBに問い合わせてデータが正しいかを検証するという方法もあります。もちろん正となるデータを用意しておいてそれと比較するというのもありです。

あと、ここではPHPUnitを使いましたが、データを相手にテストを行うので別にSimpleTestでも良いですし、PerlでもRubyでも何でもOKです。Javaで開発したシステムのデータをPHPで検証する、なんてこともできますね。(LL言語の利用範囲がこんなところにも;-))

ロジックの自動テストも大事ですが、生成物の自動テスト(チェック)もあると心強いものです。

livedoor Readerで「ピンを開く」数を増やす

この記事の所要時間: 051

これ何気に困ってました。Firefoxのポップアップブロック数制限によりlivedoor Readerで「ピンを開く」を実行しても20画面以上開けませんでした。いつのまにかピン数が15を超えるとピンするのを躊躇したり。。。

で、livedoor Readerでブロックされるという件だけど、キー操作でウィンドウを開くってのはユーザーが起こしたイベントに紐付いたwindow.openではあるのだけれど、タイマーを使って順次開いたりしているので、ブラウザ側で判別するのは難しいだろうと思う。なんにせよサイトで許可していても開かないことがある、っていうのは、説明しづらくて非常に困る感じがする。(今のところabout:configやら拡張やらでdom.popup_maximumを増やすしか無いようだ。)

最速インターフェース研究会 :: Firefox2のポップアップブロックについて調べてみた

ありがたや。これで気にせずにピンを付けられます。;-)

Nesting level too deep – recursive dependency?

この記事の所要時間: 22

Big Room » Blog Archive » PHP 5.2 - Nesting level too deep - recursive dependency?でオブジェクト同士の比較で発生するエラーについて書かれています。

class MyObj
{
    public $p;
}
class OtherObj
{
    public $q;
}

$a = new MyObj();
$b = new OtherObj();
$a->p = $b;
$b->q = $a; // the circular reference: $a->p->q === $a

$c = new MyObj();
$d = new OtherObj();
$c->p = $d;
$d->q = $c;// another circular reference: $c->p->q === $c

echo ( $a == $c ); // Fatal error:
?>
Big Room » Blog Archive » PHP 5.2 - Nesting level too deep - recursive dependency? (2006-11-16)

ポイントは循環参照と[==]での比較です。

PHPでは[==]と[===]とで処理結果が異なるのは良く言われていることですが、これはオブジェクト同士でも同様です。オブジェクトの比較にあるように[==]ならオブジェクトの持つ変数同士が比較され、[===]ならオブジェクトの参照が比較されます。(Javaの==とStrings#equals()に似ていますね。)

つまり循環参照しているオブジェクト同士を[==]で比較すると延々オブジェクトの変数が比較され続けてエラーが発生するというわけです。

ちなみにPHP4では[==]も[===]もPHP5の[==]と同じ動作をするようです。(PHP4におけるオブジェクトの比較

呼び出し元のメソッドを使用したい場合にうっかり循環参照になっていたりするので注意が必要です。

 

# エントリではPHP5.1.6では[==]で動作していたようですが、手元の環境では5.1.6でも同じエラーが発生しました。5.2.0特有の現象というわけでは無さそうです。

CakePHP1.1.10.3825-bake.php修正パッチ

この記事の所要時間: 246

cakephp.jpにあるように、1.1.10.3825のbake.phpをWindows上で使用すると上手く動作しない場合があります。

この現象はcake\とは異なるディレクトリにプロジェクトを生成しようとした場合に発生するようです。

New App Directoryが作成するプロジェクトのディレクトリなのですが、妙なパスが表示されています。[D:\tmp\d:\tmp\app]

CODE:
  1. > php -q D:\tmp\cake\scripts\bake.php -project d:\tmp\app
  2. ...
  3. New App Directory: D:\tmp\d:\tmp\app
  4. ---------------------------------------------------------------

原因はディレクトリ区切り文字を[/]に決めうちしているためです。この問題の修正パッチを作りましたのでよろしければどうぞ。

CODE:
  1. --- cake_1.1.10.3825.org/cake/scripts/bake.php       2006-11-10 14:37:00.531250000 +0900
  2. +++cake_1.1.10.3825/cake/scripts/bake.php       2006-11-13 18:08:16.187500000 +0900
  3. @@ -82,12 +82,12 @@
  4.         }
  5.  
  6.         $shortPath = str_replace($root, '', $app);
  7. -       $shortPath = str_replace('../', '', $shortPath);
  8. -       $shortPath = str_replace('//', '/', $shortPath);
  9. +       $shortPath = str_replace('..' . DS, '', $shortPath);
  10. +       $shortPath = str_replace('//', DS, $shortPath);
  11.  
  12. -       $pathArray = explode('/', $shortPath);
  13. +       $pathArray = explode(DS, $shortPath);
  14.         $appDir = array_pop($pathArray);
  15. -       $rootDir = implode('/', $pathArray);
  16. +       $rootDir = implode(DS, $pathArray);
  17.         $rootDir = str_replace('//', '', $rootDir);
  18.  
  19.         if(!$rootDir) {

 

2006/11/21追記:
修正パッチを取り込んで貰えました。https://trac.cakephp.org/changeset/3872
他にも修正されていますし、いずれ1.1.11が出そうですね。

日付時間はDateTimeクラスで

この記事の所要時間: 357

5.2.0の新機能にDateTimeクラスがあります。既存のdate関係の関数をクラスにまとめたもののようです。

以前から思っていたのですが、日付時間データを文字列や数値で持っているのに違和感がありました。特にDBからデータを取得した際、DBで日付時間型(PostgreSQLならtimestamp型)のものはPHPでもそのまま扱えないかな、と思っていました。

そこで自分が構築しているアプリケーションではDBのレコードをModelに格納する際に自作のDateクラスに変換しています。こうしておけばそのModelを使う場合は日付時間をDateクラスのメソッドで操作できます。イメージとしては以下のような感じです。

PHP:
  1. <?php
  2. class MyModel {
  3.   /** 登録時間 */
  4.   var $regiterTime = null;
  5. }
  6.  
  7. // DB からモデル生成
  8. // ....
  9.  
  10. // 年だけを取得
  11. $model->registerTime->getYear();
  12. // YYYY/MM/DD文字列で取得
  13. $model->registerTime->getFormatString('Y/m/d');
  14. ?>

日付時間は色々なフォーマットに変換して表現する(YYYY/MM/DDにしたりYYYYだけ取ったり)のでクラス化した方が後は扱いやすいと思うのですが、あまりこういった話は聞かないですね。DBの日付時間型関数が充実しているだけにPHPでも、と考えてしまいます。

そこで冒頭のDateTimeクラスです。

いずれはPDOを使ったDBアクセスではDBの日付時間型をこのクラスに変換してくれるようになって、日付時間はDateTimeクラスで扱うというのが標準になってくれれば良いなあ、と淡い期待を抱いています。

常にDateTimeクラスにされると困る人もいるでしょうから、イメージとしてはこんな感じでしょうか。

PHP:
  1. <?php
  2.   // createdの内容をDateTimeオブジェクトにする
  3.   $stmt->bindColumn('created', $created, PDO::PARAM_DATE);
  4. ?>

ひそかに注目のクラスです。

 

2006/11/10追記:PDOとの連携を考えてみました。

かなりadhocな方法ですが、いちおうDBの日付時間型をDateTimeクラスで表現できます。

PHP:
  1. <?php
  2. class MyModel {
  3.   private function __set($name, $value) {
  4.     if ($name == 'created') {
  5.       $this->{$name} = new DateTime($value);
  6.     } else {
  7.       $this->{$name} = $value;
  8.     }
  9.   }
  10. }
  11.  
  12. dl('php_pdo_pgsql.dll');
  13. try {
  14.   $dbh = new PDO('pgsql:host=localhost;dbname=xxx', 'xxx', 'pass');
  15.   $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  16.   $stmt = $dbh->prepare('SELECT id,created from users where id=?');
  17.  
  18.   $stmt->execute(array('1'));
  19.  
  20.   while ($ret = $stmt->fetchObject('MyModel')) {
  21.     printf("id:%d %04d年%02d月%02d日", $ret->id
  22.                                     , $ret->created->format('Y')
  23.                                     , $ret->created->format('m')
  24.                                     , $ret->created->format('d'));
  25.   }
  26.  
  27.   $dbh = null;
  28. } catch (PDOException $e) {
  29.    print "error!: " . $e->getMessage() . "<br />";
  30.    die();
  31. }
  32. ?>

Home > アーカイブ > 2006-11

検索
フィード
メタ情報

Return to page top