Hatena::ブログ(Diary)

kunitの日記 このページをアンテナに追加 RSSフィード

2008/02/05 (火)

[][] 言葉重要 - テストということについて 10:37  言葉重要 - テストということについてを含むブックマーク

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や、RubyRSpecとかがあって、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だとそれが目の前のクラスに対して直接記述をするわけで、「こいつがこう動いてくれればなぁ」と言いながらできるんではないかと思います。(最後はプロダクトの宣伝かよ > 自分)

[] やっとかけた・・・ 01:32  やっとかけた・・・を含むブックマーク

書いたものを実際に全部実行してためしてたので、えらい時間がかかりました・・・ひとまずDocTestを使ってもらえるようにこれでなりました。

今までありそうでなかったものではないかと思うんですが、いかがでしょうか?(もしかしてあったりする?)

Maple4はこれを使って今後TDDで開発されるでしょう(なぜか他人事)。

実はいくつか説明し切れてないところがあるんですが、まぁそれはおいおいということで。使ってもらった結果、こうなるともっと現場で使いやすくなるよ!というものがあれば、いってください。

今月のPHP勉強会のネタはこれでやろうと思いますので、よろしくお願いします。

追記(02:24)
きちんと言及すべきことを忘れてました。このDocTestというのは、rhacoがそういう機構をもっているということをしってから、私なりに噛み砕いて考えたものです。rhacoがそういうものをもっていなければ出来なかったものです。元の発想をいただいただけではなく、名前までそのままですみません。DocTestという言葉はわかりやすい言葉だと思ってますので、できればこのまま使わせてください。

[] テスト厨になりたいあなたのための、DocTest 01:29  テスト厨になりたいあなたのための、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を使った開発は以下のような手順を踏むことになります。

  1. とりあえずクラス宣言をする
  2. メソッドのインタフェースを決める
  3. そのメソッドをどのように呼びたいのかということをDocコメントに規約どおりに書く
  4. テストを実行する
  5. まだ処理を書いてないのでテストが失敗する
  6. テストが通る最低限度のコードを書く
  7. テストを実行する
  8. 今度はテストは成功する
  9. 他の使い方がないかテストを追加してみる(引数の値を変えてみる等)
  10. 場合によっては最低限度のコードでは動かなくてテストが失敗する
  11. テストが成功するまで(6)にもどる。成功したら次へ
  12. 使いたいパターンが一通り成功したらそのメソッドはひとまず完成
  13. 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メソッドでテスト実行となりますが、引数としてテスト対象となるディレクトリを指定する。
  • 指定したディレクトリ以下のファイルを再帰的にチェックし、前回チェック時から変更のあるファイルのみテストケースファイルの生成を行って、テストが実行される。
  • ファイルおよびクラスの命名規則はPEARZend 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を使って、みんなでテスト厨になりましょう。

ここまで長々と見ていただいてありがとうございました。

いつきいつき 2008/03/12 10:28 はじめまして。こちらの記事に興味を持ったので”Maple_DocTest_alpha2.zip”を試させてもらおうと思ったのですがダウンロードできないようです・・・。私だけでしょうか。。。

kunitkunit 2008/03/12 15:44 すみません。現在、kunit.jp以下のディレクトリ構成を大幅変更中で
リンクが切れてました。

暫定的にリンクをつなぎましたので、こちらから再度ダウンロードを
行ってみてください。

いつきいつき 2008/03/13 01:27 リンク修正ありがとうございます。早速試させて頂きました。
PHPでのテストの価値観が変わるくらいの衝撃でした。これはホントに感動しました。気付いた頃にはテスト厨になってそうです。

kunitkunit 2008/03/13 07:33 おお、ありがたいお言葉。励みになるなぁ。
DocTestはプログラミングが「楽しく」なるおまじないみたいな
ものだと思ってます。
まだまだこのツールはよくなりますよー。
今月末くらいにお披露目ができると思うので乞うご期待。