2008/02/05 (火)
■[Maple][TDD] 言葉重要 - テストということについて

DocTestのお披露目がひとまず完了しましたが、そもそも「テスト」という言葉はプログラマーの嫌いな言葉の一つではないかと思ってます。
ひとしきり思考をめぐらせて、テクニックを駆使した後にくる面倒な時間・・・というイメージがどうしてもあるんではないかと。
「テスト」という言葉の呪縛は、「いや実はそうでなくて、もっと効率を上げるために不可欠なものなんですよー」っていっても一度ついたイメージはなかなか払拭できない。
ということで「テスト」と呼ぶのをやめてみたらってので、TDD(Test Driven Development、テスト駆動開発)ではなくて、BDD(Behavior Driven Development、ビヘイビア駆動開発)としようというするものが実はだーいぶ前からあるんですよね。えらそうに書いてますが、今googleさんに聞いてみたら、2005年の平鍋さんの記事が見つかった。うーむ、相当遅れてるよ、私・・・
http://blogs.itmedia.co.jp/hiranabe/2005/10/tdd__bdd__731d.html
そもそもTDDでやろうとしているものは後工程でやっているテストではなくて、設計なのだと。確かにそれは回帰テストとしての役割もあるのでテストと呼んでもよいのだが、意識を変えるためにビヘイビア=振る舞いを先に決定しているのだと考えようと。呼称を変えることによって意識をかえる、これ重要ってことです。(最近はなににつけても、「名づけ重要」だなぁと思ってます。このあたりの話はまた別にします)
平鍋さん、和田さん、角谷さんといった方々は既にもう数年前にそこにいて、それを踏まえていろいろやってらっしゃるわけですから、がんばって走らないとおいつけないっすね。
ちなみに、BDD用のツールというのはJavaのjBehaveや、RubyのRSpecとかがあって、IBM dwの記事や、角谷さんが解説されているるびまのRSpecの記事は大いに参考になると思います。
http://www.ibm.com/developerworks/jp/java/library/j-cq09187/index.html
http://jp.rubyist.net/magazine/?0021-Rspec
角谷さんの記事にある、振る舞いは日本語で書こうというのもなかなかおもしろいなぁと。DocTestもMaple4本体はそうしないけど、現場レベルの開発ではテスト名は日本語でつけていけば失敗したときに「ああ、あそこか・・・」と直感的にわかっていいんではないかと。日本語でテスト名をつけようというのは、DocTestの説明のところに書いた和田さんのムービーやWEB+DBの記事でも紹介されてますね。
ちなみにPHPにもPHPSpecってのがあります。恐ろしく速いスピードでいろいろ翻訳されている高木さんによる日本語訳マニュアルもあります。(本家側からいけるものにリンクをはってもいいんだけど、高木さんの功績をたたえて、そちらにリンク)
http://www.m-takagi.org/docs/php/phpspec/
また、日本語でテストしてみる?ってのは、Seasar.PHPのkloveさんがちらっと試されてますね。これもなかなか興味深い。
http://cgi39.plala.or.jp/klove/w/k.cgi?page=Diary%2F2007%2D11%2D20
あと、Piece Frameworkのテストツール Stagehand_TestRunner の最新版 2.0.0 ではPHPSpecへの対応が盛り込まれていて、Piece自体をチラッと覗いてみると、ぐいぐいPHPSpecでのテストへ移行してる感じですね。さすが久保さん、目のつけどころとスピードは相変わらずすごい。
http://piece-framework.com/2008/01/stagehand_testrunner_200_stabl_1.html
まだ「テスト」と聞くと・・・という方は、処理を書く前にそのメソッドの振る舞いをきめる設計をしてるんだと思ってやってみませんか? DocTestだとそれが目の前のクラスに対して直接記述をするわけで、「こいつがこう動いてくれればなぁ」と言いながらできるんではないかと思います。(最後はプロダクトの宣伝かよ > 自分)
■[Maple] やっとかけた・・・

書いたものを実際に全部実行してためしてたので、えらい時間がかかりました・・・ひとまずDocTestを使ってもらえるようにこれでなりました。
今までありそうでなかったものではないかと思うんですが、いかがでしょうか?(もしかしてあったりする?)
Maple4はこれを使って今後TDDで開発されるでしょう(なぜか他人事)。
実はいくつか説明し切れてないところがあるんですが、まぁそれはおいおいということで。使ってもらった結果、こうなるともっと現場で使いやすくなるよ!というものがあれば、いってください。
今月のPHP勉強会のネタはこれでやろうと思いますので、よろしくお願いします。
■[Maple] テスト厨になりたいあなたのための、DocTest

昨日のエントリで公開しますよと言ってたMaple4として最初のプロダクト DocTest の alpha1 をリリースします。ただし、以下のようなものだと思ってください。
- 追記(2/29現在)
- alpha1⇒alpha2になってます。
- まだMaple4 Project内でも実戦投入していないものなので、ダウンロードしてもお試し程度に使うというのでとどめてください。(DocTest自体のテストは一通りしているつもりですが、リリース直前にちょっと試したらいろいろ出てきたのでまだまだ残ってるかも・・・)
- PHPUnit3をインストールしないといけないので、自分専用の環境等でお試しください(何かあってはいけない環境では試さないでください。まぁ一応念のため)
- 今回はひとまずPHP License 3.01での配布となりますが、Maple4自体のライセンスをどうするのかが議論中なので、次のリリースからはライセンスが変わるかもしれない(BSD系にしようかという話がコミッター間で行われてます)
- ディレクトリ構成やインタフェースは次のリリースで変わるかもしれない
- PHP5.1.6およびPHP5.2.5で動作確認をしています。
- もしよろしければ、使ってみた感想をお寄せください。次のリリースに要望等が反映されると思います。
いろいろ言い訳が続きましたが、DocTestというものがどういうものかを説明したいと思います。
DocTestは以下のようなプロダクトになります。
- TDD(テスト駆動開発)を支援します。
- DocTestはPHPUnit3もしくはsimpletestのラッパーで、そのどちらかを使用してテストを実施します(デフォルトはPHPUnit3)。
- PHPUnit3/simpletestを使ったテストでは通常テスト用のファイルを別の場所につくってからテストを行いますが、テストをしたいクラス内にDocコメントとしてテスト内容を記述しながらテストおよび開発を進めます(テスト用のクラスはDocTestが自動的に切り出してファイルに出力し、それを使ってテストが行われます)。
- Docコメントに書いた内容をPHPDocumentorで出力すれば、テスト内容がAPIリファレンスに反映されます。これにより、APIリファレンスを見ることによってクラスの仕様が読み取れます。
テスト駆動開発のことを説明し始めたらそれだけで終わってしまいますので、是非以下のムービーをご覧ください。全部見ると結構な時間になりますが、TDDってなに?ってのを講師である和田さんがものすごく丁寧に説明されてます。
http://gihyo.jp/dev/serial/01/tdd
DocTestを使った開発は以下のような手順を踏むことになります。
- とりあえずクラス宣言をする
- メソッドのインタフェースを決める
- そのメソッドをどのように呼びたいのかということをDocコメントに規約どおりに書く
- テストを実行する
- まだ処理を書いてないのでテストが失敗する
- テストが通る最低限度のコードを書く
- テストを実行する
- 今度はテストは成功する
- 他の使い方がないかテストを追加してみる(引数の値を変えてみる等)
- 場合によっては最低限度のコードでは動かなくてテストが失敗する
- テストが成功するまで(6)にもどる。成功したら次へ
- 使いたいパターンが一通り成功したらそのメソッドはひとまず完成
- PHPDocumentorを使ってAPIリファレンスを出力する
ひとまず概要はここまでで実際に使ってみましょう。
まず、PHPUnit3のインストールが必要となります。以下の要領でインストールしてください(場合によってはPEAR自体のバージョンをあげないといけないかもしれません)。
pear channel-discover pear.phpunit.de pear install phpunit/PHPUnit
さて、次はDocTestを取得してください。DocTestのalpha2は以下のURLからダウンロードできます。
http://kunit.jp/old/archives/Maple_DocTest_alpha2.zip
一応ZIP圧縮としました。(tar.gzでほしいと思っている人は多分ZIPでも何とかなるだろうという安易な考えてこうしてます。すみません)
では、圧縮ファイルを展開して、以下のようなディレクトリにしたものとして説明を続けます。
- c:\temp\DocTest というディレクトリをつくる
- そのディレクトリ以下に展開したもののうち、srcディレクトリだけコピー
- 同じディレクトリに classes ディレクトリを作成
- 同じディレクトリに tests_c ディレクトリを作成
- 同じディレクトリに docs ディレクトリを作成
ここまでで以下のようなディレクトリ構成になっているはずです。(これ以降の説明では「c:\temp\DocTest\」以下のものとして説明します)
| c:\temp\DocTest\src\ | DocTestが入ってる |
| c:\temp\DocTest\classes\ | これからテストをするクラスを書いていく |
| c:\temp\DocTest\tests_c\ | DocTestが使用するディレクトリ |
| c:\temp\DocTest\docs\ | APIリファレンスを出力するディレクトリ |
まず、DocTest自体を起動するphpファイルを作成しましょう。doctest.phpという名前で以下の内容のものを作ってください。
<?php error_reporting(E_ALL|E_STRICT); require_once 'src/Maple/DocTest.php'; $params = array( 'compileDir' => dirname(__FILE__) . '/tests_c/', ); $testDir = dirname(__FILE__) . '/classes/'; Maple_DocTest::singleton($params)->run($testDir); ?>
- 追記(13:10)
- error_reportingの設定をいれました。PHP5の開発ならば、いれとかないとね。その代わり、エラー時の表示がちょっと変わるかも・・・
DocTest自体のインタフェースは以下のようなものとなります。
- singletonとして生成する際にパラメータとしてcompileDirというのを指定する。指定したディレクトリに各クラスのコメントから取り出したテストケースが含まれるクラスファイルが出力される。
- runメソッドでテスト実行となりますが、引数としてテスト対象となるディレクトリを指定する。
- 指定したディレクトリ以下のファイルを再帰的にチェックし、前回チェック時から変更のあるファイルのみテストケースファイルの生成を行って、テストが実行される。
- ファイルおよびクラスの命名規則はPEARやZend Framework等が規約としているパターンとする。つまり、Foo_Bar_Bazクラスが、Foo/Bar/Baz.php として存在してなければなりません。
さて、doctestの起動ファイルができたら、ひとまず実行してみましょう。コマンドライン版のphpにパスを通しておいて、コマンドプロンプト等から以下のように実行します。
php doctest.php
まだテスト対象がないのでなにも出力されません。ここでエラーが発生したら、PHPUnit3のインストールがうまくいってないとか、実行ディレクトリが違うとかというような問題が発生していると思いますので、チェックをお願いします。
さて、テスト対象のファイルを作ってみましょう。classes\Example.php として以下のファイルを作りましょう。
<?php /** * DocTest Example */ class Example { /** * 挨拶をしてもらおう * * #test say * <code> * $obj = new Example; * $this->assertEquals('Hello, Maple!', $obj->say()); * </code> * * @return srting 挨拶の文字列 * @access public */ public function say() { } } ?>
気絶しそうなくらいベタなサンプルですが、挨拶をさせるというものとしたいと思います。メソッド呼び出し($obj->say())をすると「Hello, Maple!」が返却されると期待している(assertEqualsは2つの引数が同じことを期待している)というテストになります。
クラスの定義、メソッドのインタフェースも一旦きめて、テストも書いてみました。DocTestのコメント(これ以降DocTestコメントといいます)は以下のような規約があります(ここで全部はいってません。後で続きの規約があります)
- Docコメント(/** で始まり */で終わるもの)の中に記述する
- @で始まる識別子より前に書く
- #test [テスト名] <code> ... </code> というものとして記述する。
- 上記のブロックは同じメソッドに対して何度記述しても良い
- テスト名がつけられている場合、それがテストメソッド test[テスト名]として使用される。上記の場合、生成されるメソッド名は「testSay()」になる
- テスト名が省略された場合は、そのテストが記述されているメソッド名になる。上記の場合はsayメソッドに対してのテストなので、「#test say」は「#test」と省略できる。
- テスト名に「__setup」「___teardown」と記述した場合は、上記のルールは適用されずに「setUp」「tearDown」メソッドが生成される。通常これはクラス宣言部のDocコメントとして記述する。
- テスト名に「__noop」とすると変換せずにテストケース内に出力される。
上記のルールを適用し、一つ前のクラスはこのように書き換えられます。「__noop」でベタに吐き出すものとしてプロパティ宣言をし、「__setup」「__tearDown」でオブジェクトの準備、破棄をしてます。
<?php /** * DocTest Example * * #test __noop * <code> * private $obj; * </code> * #test __setup * <code> * $this->obj = new Example; * </code> * #test __teardown * <code> * $this->obj = null; * </code> */ class Example { /** * 挨拶をしてもらおう * * #test * <code> * $this->assertEquals('Hello, Maple!', $this->obj->say()); * </code> * * @return srting 挨拶の文字列 * @access public */ public function say() { } } ?>
さてさらに規約は続きます。
- 「__setup」「__teardown」が宣言されてない場合、自動的に$objプロパティーをprivate宣言し、setUp/tearDownでの準備/破棄コードが生成される。(つまり上記の例で書いてるが書かなくてもよい)
- $this->obj->「テストしたいメソッド名」というのはよくテスト内で記述することになるので、「#f」という省略が出来る。
- $this->assertEqualsは「#eq」、$this->assertNotEqualsは「#ne」といったように「#true」「#notTrue」「#null」「#notNull」というassert系のメソッドの省略ができる。
ここまでの規約を踏まえて、上記の例は以下のようにかけます。
<?php /** * DocTest Example */ class Example { /** * 挨拶をしてもらおう * * #test * <code> * #eq('Hello, Maple!', #f()); * </code> * * @return srting 挨拶の文字列 * @access public */ public function say() { } } ?>
今回のものはこれ以上は省略形はないので、これでテストを進めます。ここでdoctest.phpを実行すると以下のような出力が得られます。
C:\temp\DocTest>php doctest.php PHPUnit 3.2.11 by Sebastian Bergmann. F Time: 0 seconds There was 1 failure: 1) testSay(Maple_DocTest_ExampleTest) Failed asserting that two strings are equal. expected string <Hello, Maple!> difference ????????????> got string <> C:\temp\DocTest\tests_c\Example.php:18 C:\temp\DocTest\src\Maple\DocTest\Phpunit3.php:52 C:\temp\DocTest\src\Maple\DocTest.php:245 C:\temp\DocTest\doctest.php:9 FAILURES! Tests: 1, Failures: 1.
もちろんテストは通りません。sayメソッドは何も返却してませんので。ではテストを通るようにしましょう。
<?php /** * DocTest Example */ class Example { /** * 挨拶をしてもらおう * * #test * <code> * #eq('Hello, Maple!', #f()); * </code> * * @return srting 挨拶の文字列 * @access public */ public function say() { return 'Hello, Maple!'; } } ?>
今度はテストが通ります。OKだけでるそっけない結果が成功ということになります。
C:\temp\DocTest>php doctest.php PHPUnit 3.2.11 by Sebastian Bergmann. . Time: 0 seconds OK (1 test)
で、ここで終わりではなくて、引数に名前を渡したら、その人の名前で挨拶をしてもらいましょう。
<?php /** * DocTest Example */ class Example { /** * 挨拶をしてもらおう * * #test * <code> * #eq('Hello, Maple!', #f()); * #eq('Hello, kunit!', #f('kunit')); * </code> * * @return srting 挨拶の文字列 * @access public */ public function say() { return 'Hello, Maple!'; } } ?>
ここでまたdoctest.phpを実行します。もちろん処理を変更してないので、テストは失敗します。
C:\temp\DocTest>php doctest.php PHPUnit 3.2.11 by Sebastian Bergmann. F Time: 0 seconds There was 1 failure: 1) testSay(Maple_DocTest_ExampleTest) Failed asserting that two strings are equal. expected string <Hello, kunit!> difference < xxxxx> got string <Hello, Maple!> C:\temp\DocTest\tests_c\Example.php:19 C:\temp\DocTest\src\Maple\DocTest\Phpunit3.php:52 C:\temp\DocTest\src\Maple\DocTest.php:245 C:\temp\DocTest\doctest.php:9 FAILURES! Tests: 1, Failures: 1.
では、処理をきちんと書きましょう。
<?php /** * DocTest Example */ class Example { /** * 挨拶をしてもらおう * * #test * <code> * #eq('Hello, Maple!', #f()); * #eq('Hello, kunit!', #f('kunit')); * </code> * * @param string 名前(デフォルトMaple) * @return srting 挨拶の文字列 * @access public */ public function say($name = 'Maple') { return "Hello, {$name}!"; } } ?>
今度はテストが通ります。そっけないOKがでました。
C:\temp\DocTest>php doctest.php PHPUnit 3.2.11 by Sebastian Bergmann. . Time: 0 seconds OK (1 test)
ちなみに、以下のように変更しても実行できます。
<?php /** * DocTest Example */ class Example { /** * 挨拶をしてもらおう * * #test sayDefault * <code> * #eq('Hello, Maple!', #f()); * </code> * #test sayName * <code> * #eq('Hello, kunit!', #f('kunit')); * </code> * * @param string 名前(デフォルトMaple) * @return srting 挨拶の文字列 * @access public */ public function say($name = 'Maple') { return "Hello, {$name}!"; } } ?>
これでもテストは成功します。ただし「sayDefault」「sayName」という2つのテストメソッドができたので、出力が上部の「.」が「..」に(ドット1つがテストメソッドにあたるので)、「OK(1 test)」から「OK(2 tests)」にかわります。
C:\temp\DocTest>php doctest.php PHPUnit 3.2.11 by Sebastian Bergmann. .. Time: 0 seconds OK (2 tests)
さて一通りテストが終わって、これで機能がそろったとします(しょぼすぎですが・・・)。ここでPHPDocumentorを動かしてみましょう。PHPDocumentorも以下のようにしてインストールしてください。
pear install --alldeps phpdocumentor
PHPDocumentorのインストールが終わったら以下のコマンドを入力してみましょう。
c:\temp\DocTest>phpdoc -t ./docs -d ./classes -o HTML:Smarty:PHP
そうするとバリバリと動いて無事APIリファレンスが出力されます。
出力されたものが以下のものです。
http://kunit.jp/old/DocTest-alpha1-example/
APIリファレンスのclassesにある「Example」をみてください。先ほど書いたDocTestコメントがきちんと表示されます。これを見ることにより、この引数を渡せばこれが返ってくるというインタフェースがテストコードから読み取れるようになります。しかもこれは実行したものですので、間違いなく動くインタフェースです。
テストをするのにいちいちテストを記述した別ファイルを編集するということになるとリズムが悪くなりますし、新しいインタフェースの仕様を別のドキュメントに反映するというのもやっぱり面倒になってくるのではないでしょうか。
このようにテストおよび開発があっちこっち行かなくてもそのクラスだけを見ながら行えて、開発終了後には動作確認済みのインタフェース仕様がドキュメントに反映されるということになれば、テストを行うこと自体を楽しみながら開発ができるのではないかと思います。まずは自分がそのクラスの最初の使用者になってテストを書き、そうなるように処理を書き、テストしたら通る。そのリズムで進めばテストは楽しいものになると思います。
DocTestを使って、みんなでテスト厨になりましょう。
ここまで長々と見ていただいてありがとうございました。
- [Maple][開発][Usagi]DocTestのリリース
- [PHP][開発環境]VimでDocTestを使ってTDDに挑戦してみる
- WEB開発日記 - テスト厨になりたいあなたのための、DocTest - kunit...
- より良い環境を求めて - アクションのテスト
- CODE NAME: TUNE34 - テスト駆動開発ツール DocTest
- kunitの日記 - DocTest alpha2
- syttruの日記 - PHP勉強会に行ってきました(後半)
- kunitの日記 - OSC2008 DocTest
- kunitの日記 - DocTestのリンクがきれてましたね・・・
- p4lifeのメモ - PHP のテスティングフレームワーク
リンクが切れてました。
暫定的にリンクをつなぎましたので、こちらから再度ダウンロードを
行ってみてください。
PHPでのテストの価値観が変わるくらいの衝撃でした。これはホントに感動しました。気付いた頃にはテスト厨になってそうです。
DocTestはプログラミングが「楽しく」なるおまじないみたいな
ものだと思ってます。
まだまだこのツールはよくなりますよー。
今月末くらいにお披露目ができると思うので乞うご期待。