5.2. コードを書く前に、まずは要求される振る舞いを定義する

新しいアプリケーションを開発するにあたり、監査証跡を記録するロギングシステム が必要となりました。 既存のオープンソースのロガーライブラリを調べてみたところ、 要件を満たすライブラリが見つからないようです。 そこで要件を満たすライブラリを自前で作成することにしました。 実際に作成しはじめる前に、まずその要件をはっきりさせなければなりません。 言いかえれば、そのライブラリがどのように振る舞ってほしいのかをはっきりさせるということです。 他のメンバーと相談した結果、まず最低限の基本機能が確定しました。 それは、メッセージをファイルシステムに記録するということです。

さっさとエディタを立ち上げてコードを書きはじめたい気持ちはわかりますが、 まずは仕様 を書くことからはじめましょう。

例 5.2. プレーンテキストで書いた、ファイルシステムロガーのスペック

New Filesystem Logger:
(新しいファイルシステムロガー)
- should create a new log file if none currently exists
  (は、ログファイルが存在しない場合は新規ファイルを作成しなければならない)
- should use an existing log file if one exists without truncating it
  (は、ログファイルが存在する場合は、既存の内容を残したままそのファイルを使用しなければならない)
- should throw Exception if existing log file not writeable
  (は、ログファイルに書き込めない場合には例外をスローしなければならない)

このシンプルなプレーンテキストの仕様を PHPSpec 形式に変換するには、 新しいコンテキストクラスを作成して その振る舞いを表すサンプルを定義します。

class DescribeNewFilesystemLogger extends PHPSpec_Context
{

    public function itShouldCreateCreateNewLogFileIfNoneExists()
    {
        $this->pending();
    }

    public function itShouldUseAnExistingLogFileIfOneExistsWithoutTruncatingIt()
    {
        $this->pending();
    }

    public function itShouldThrowExceptionIfExistingLogFileNotWriteable()
    {
        $this->pending();
    }

}

この雛形クラスでは、未確定の (pending) サンプルが定義されています。 未確定とは、まだ完成していないなどの状態を意味します。 このスペックを NewFilesystemLoggerSpec.php というファイル (もうひとつのファイル命名規約を用います。先頭の "Describe" を省略して最後に "Spec" を付加します) に保存してコマンドラインから実行すると、その出力は次のようになります。

PPP

Finished in 0.0468921661377 seconds

3 examples, 0 failures, 3 pending

PHPSpec を実行する際のコマンドラインは次のようになります。

phpspec NewFileSystemLoggerSpec

先ほど定義した仕様にもとづいて、 これらのサンプルメソッドの中身を作成していきましょう。

例 5.3. New Filesystem Logger コンテキストの仕様

class DescribeNewFilesystemLogger extends PHPSpec_Context
{

    public function itShouldCreateCreateNewLogFileIfNoneExists()
    {
        $file = $this->getTmpFileName();
        $logger = new Logger($file);
        $this->spec(file_exists($file))->should->beTrue();
    }

    public function itShouldUseAnExistingLogFileIfOneExistsWithoutTruncatingIt()
    {
        $file = $this->getTmpFileName();
        file_put_contents($file, 'Hello' . "\n");
        $logger = new Logger($file);
        $this->spec(file_get_contents($file))->shouldNot->beEmpty();
    }

    public function itShouldThrowExceptionIfExistingLogFileNotWriteable()
    {
        $file = $this->getTmpFileName();
        file_put_contents($file, 'Hello' . "\n");
        $this->spec('Logger', $file)->should->throw('Exception');
    }

    public function after()
    {
        unlink($this->getTmpFileName());
    }

    public function getTmpFileName()
    {
        return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'logger_tmp_file.log';
    }

}

これで、最初にプレーンテキストで定義したスペックを実行可能なコードサンプルに落とすことができました。 もちろん、今これを実行しても単に Fatal Error となるだけでしょう。 まだ Logger クラスが存在しないわけですから。 この続きは、また後ほど。

5.2.1. PHPSpec におけるスペックの配置

先ほど作成した新しいファイルシステムロガーのサンプルを見れば、スペック をどのように作成すればいいのかがわかります。

  1. すべてのスペックは PHPSpec_Context のサブクラスに記述し、 システムの仕様を表す条件をここに集約する

  2. コンテキストクラス名の最初は必ず "Describe" となり、 その後に内容を表す文を続ける

  3. コンテキスト内のサンプルメソッド名の最初は必ず "itShould" となり、 その仕様を表す説明文をできるだけきちんとした文で書くようにする (現在形で仕様を書くために、"Should" を省略できるようにする可能性もある)

  4. PHPSpec_Context::spec() メソッドを使用して、 DSL 経由で使用するオブジェクトやスカラー値を準備する

  5. ドメイン特化言語 (DSL) は一般的に Expectation (should/shouldNot) と Matcher (beSomething, haveSomething, equals, etc.) で構成される

  6. 正式なルールではないが、ひとつのサンプルではひとつのスペックのみを扱うようにする - これにより、各スペックが個別の振る舞いを表すようになる

  7. getTmpFileName() のように、サンプル以外のメソッドをクラスに追加して ヘルパーメソッドとして使用できる

  8. after() メソッドおよび before() メソッドを使用して、 各サンプルで共通のフィクスチャを準備できる

  9. afterAll() メソッドおよび beforeAll() メソッドを使用して、 全サンプルの実行の前後に一度だけ実行する処理を定義できる

  10. サンプルの内部で例外やエラーを発生させても、 それがその他のテストの実行を妨げることはない

5.2.2. New Filesystem Logger の仕様を実装するコード

PHPSpec で書いた仕様をもとに、 その仕様を満たすロガーの実装を始めましょう。 きっとリファクタリングのことを考える人もおられるのでしょうが、 ここではまず、仕様を満たす必要最小限のコードを書くことだけを考えます。

例 5.4. ファイルシステムロガーの実装

class Logger
{

    protected $_file = null;

    public function __construct($file)
    {
        if (!file_exists($file)) {
            $f = fopen($file, 'w');
            fclose($f);
        } elseif (file_exists($file) && is_writeable($file)) {
            $this->_file = $file;
        } else {
            throw new Exception('ログファイルに書き込めません'); 
        }
    }

}

次に、これら以外にどんな振る舞いがあるのかを考えて それを表すスペックを書いていきましょう。 Exception クラスを継承した Logger_Exception クラスを作成しますか? ファイルのチェックをもう少し厳しくしますか? ファイルの処理を新たなサブクラスに移したり、 あるいはストラテジークラスを使用したりしますか?

何をやるにしても、コードを書き始める前にまずスペックを書くようにします。 小さなことからコツコツと進め、少しずつクラスを作成していくようにしましょう。 また、仕様以上のことをコードに書かないよう心がけましょう。 ファイル処理を別のクラスに抽出することにしたとしても、 (その価値が十分あると保証できる場合を除いて) すぐに新しいクラスの仕様を考えることはありません。というのも、 もとのスペックにおいても ロガーを作成する際にファイルを指定するということが網羅されているからです。 この場合は新たな振る舞いを追加するのではなく、 単にその振る舞いに関する実装を透過的に変更するということになります。