テスト厨になりたいあなたのための、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\」以下のものとして説明します)

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 
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 
difference      <       xxxxx>
got string      
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を使って、みんなでテスト厨になりましょう。

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