第9章 テストダブル

Gerard Meszaros は、テストダブルの概念を [Meszaros2007] でこのように述べています。

 

Sometimes it is just plain hard to test the system under test (SUT) because it depends on other components that cannot be used in the test environment. This could be because they aren't available, they will not return the results needed for the test or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control or visibility of the internal behavior of the SUT.

- テスト対象のシステム (SUT: system under test) をテストすることは、時に非常に困難なこととなります。というのも、 システムが他のコンポーネントに依存しており、 そのコンポーネントをテスト環境で利用できないことがあるからです。 そもそも使用不可能であったりテストで必要な結果を返さなかったり、 あるいは好ましくない副作用があったりといったことです。 それ以外の場合も、テスト環境の内部的な振る舞いをきちんと制御して 目に見えるようにしておくことが必要です。

When we are writing a test in which we cannot (or chose not to) use a real depended-on component (DOC), we can replace it with a Test Double. The Test Double doesn't have to behave exactly like the real DOC; it merely has to provide the same API as the real one so that the SUT thinks it is the real one!

- 実際に依存するコンポーネント (DOC: depended-on component) を使わないテストを書く場合は、それをテストダブルで置き換えることができます。 テストダブルは、必ずしも実際の DOC とまったく同様に動作する必要はありません。 単に実際のものと同じ API を提供し、 SUT に「これは本物だ!」と思わせるだけでいいのです。

 
 --Gerard Meszaros

PHPUnit の getMock($className) メソッドを使うと、 指定した元クラスのテストダブルとして振る舞うオブジェクトを自動的に生成することができます。 このテストダブルオブジェクトは、元クラスのオブジェクトを要するすべての場面で使うことができます。

デフォルトでは、元クラスのすべてのメソッドが置き換えられて、 (元のメソッドは呼び出さずに) 単に NULL を返すだけのダミー実装になります。たとえば will($this->returnValue()) メソッドを使うと、 ダミー実装がコールされたときに値を返すよう設定することができます。

制限

final, private および static メソッドのスタブやモックは作れないことに注意しましょう。 PHPUnit のテストダブル機能ではこれらを無視し、元のメソッドの振る舞いをそのまま維持します。

警告

パラメータの管理方法が変わったことに気をつけましょう。 以前の実装ではオブジェクトのすべてのパラメータをクローンしており、 あるメソッドに渡されたオブジェクトが同じものであるかどうかを確かめることができませんでした。 例 9.14 に、新しい実装の活用例を示します。 例 9.15 に、以前の挙動に戻す方法を示します。

スタブ

実際のオブジェクトを置き換えて、 設定した何らかの値を (オプションで) 返すようなテストダブルのことを スタブ といいます。 スタブ を使うと、 「SUT が依存している実際のコンポーネントを置き換え、 SUT の入力を間接的にコントロールできるようにすることができます。 これにより、SUT が他の何者も実行しないことを強制させることができます。」

例 9.2 に、スタブメソッドの作成と返り値の設定の方法を示します。まず、 PHPUnit_Framework_TestCase クラスの getMock() メソッドを用いて SomeClass オブジェクトのスタブを作成します (例 9.1)。 次に、PHPUnit が提供する、いわゆる Fluent Interface (流れるようなインターフェイス) を用いてスタブの振る舞いを指定します。簡単に言うと、 いくつもの一時オブジェクトを作成して、 それらを連結するといった操作は必要ないということです。 そのかわりに、例にあるようにメソッドの呼び出しを連結します。 このほうが、より読みやすく "流れるような" コードとなります。

例 9.1: スタブを作りたいクラス

<?php
class SomeClass
{
    public function doSomething()
    {
        // なにかをします
    }
}
?>


例 9.2: メソッドに固定値を返させるスタブ

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMock('SomeClass');

        // スタブの設定を行います
        $stub->expects($this->any())
             ->method('doSomething')
             ->will($this->returnValue('foo'));

        // $stub->doSomething() をコールすると
        // 'foo' を返すようになります
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>


舞台裏では、getMock() メソッドが使われたときに PHPUnit が自動的に、求める振る舞いを実装した新たな PHP のクラスを生成しています。 生成されるテストダブルクラスの設定は、 getMock() メソッドのオプションの引数を使って行います。

  • デフォルトでは、指定したクラスのすべてのメソッドが単に NULL を返すだけのテストダブルとなります。返り値を変更するには、たとえば will($this->returnValue()) を使います。

  • オプションの第二パラメータを指定すると、その配列の中に含まれる名前のメソッドだけがテストダブルに置き換えらて、その他のメソッドはそのままとなります。パラメータに NULL を渡すと、どのメソッドも置き換えません。

  • オプションの第三パラメータには、元クラスのコンストラクタに渡すパラメータの配列を渡します (デフォルトでは、コンストラクタはダミー実装に置き換えられません)。

  • オプションの第四パラメータを使うと、生成されるテストダブルクラスのクラス名を指定することができます。

  • オプションの第五パラメータを使うと、元クラスのコンストラクタを呼び出さないようにすることができます。

  • オプションの第六パラメータを使うと、元クラスの clone コンストラクタを呼び出さないようにすることができます。

  • オプションの第七パラメータを使うと、テストダブルクラスの生成時に __autoload() を無効にすることができます。

もうひとつのやり方として、生成されたテストダブルクラスの設定を モックビルダー API で行うことができます。 例 9.3 に例を示します。 モックビルダーで使えるメソッドの一覧は次のとおりです。

  • setMethods(array $methods) をモックビルダーオブジェクト上でコールすると、テストダブルで置き換えるメソッドを指定することができます。その他のメソッドの挙動は変更しません。setMethods(NULL) とすると、どのメソッドも置き換えません。

  • setConstructorArgs(array $args) をコールしてパラメータの配列を渡すと、それを元クラスのコンストラクタに渡すことができます (デフォルトのダミー実装では、コンストラクタは置き換えません)。

  • setMockClassName($name) を使うと、生成されるテストダブルクラスのクラス名を指定することができます。

  • disableOriginalConstructor() を使うと、元クラスのコンストラクタを無効にすることができます。

  • disableOriginalClone() を使うと、元クラスのクローンコンストラクタを無効にすることができます。

  • disableAutoload() を使うと、テストダブルクラスを生成するときに __autoload() を無効にすることができます。

例 9.3: モックビルダー API を使った、生成されるテストダブルクラスの変更

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMockBuilder('SomeClass')
                     ->disableOriginalConstructor()
                     ->getMock();

        // スタブの設定を行います
        $stub->expects($this->any())
             ->method('doSomething')
             ->will($this->returnValue('foo'));

        // $stub->doSomething() をコールすると
        // 'foo' を返すようになります
        $this->assertEquals('foo', $stub->doSomething());
    }
}
?>


時には、メソッドをコールした際の引数のひとつを (そのまま) スタブメソッドコールの返り値としたいこともあるでしょう。 例 9.4 は、 returnValue() のかわりに returnArgument() を用いてこれを実現する例です。

例 9.4: メソッドに引数のひとつを返させるスタブ

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnArgumentStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMock('SomeClass');

        // スタブの設定を行います
        $stub->expects($this->any())
             ->method('doSomething')
             ->will($this->returnArgument(0));

        // $stub->doSomething('foo') は 'foo' を返します
        $this->assertEquals('foo', $stub->doSomething('foo'));

        // $stub->doSomething('bar') は 'bar' を返します
        $this->assertEquals('bar', $stub->doSomething('bar'));
    }
}
?>


流れるようなインターフェイスをテストするときには、 スタブメソッドがオブジェクト自身への参照を返すようにできると便利です。 例 9.5 は、 returnSelf() を使ってこれを実現する例です。

例 9.5: スタブオブジェクトへの参照を返すメソッドのスタブ

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnSelf()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMock('SomeClass');

        // スタブの設定を行います
        $stub->expects($this->any())
             ->method('doSomething')
             ->will($this->returnSelf());

        // $stub->doSomething() は $stub を返します
        $this->assertSame($stub, $stub->doSomething());
    }
}
?>


スタブメソッドをコールした結果として、 定義済みの引数リストにあわせて異なる値を返さなければならないこともあるでしょう。 returnValueMap() を使えば、 マップを作って引数と関連付け、それを返り値に対応させることができます。 例 9.6 を参照ください。

例 9.6: メソッドにマップからの値を返させるスタブ

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnValueMapStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMock('SomeClass');

        // 値を返すための、引数のマップを作製します
        $map = array(
          array('a', 'b', 'c', 'd'),
          array('e', 'f', 'g', 'h')
        );

        // スタブの設定を行います
        $stub->expects($this->any())
             ->method('doSomething')
             ->will($this->returnValueMap($map));

        // $stub->doSomething() は、渡した引数に応じて異なる値を返します
        $this->assertEquals('d', $stub->doSomething('a', 'b', 'c'));
        $this->assertEquals('h', $stub->doSomething('e', 'f', 'g'));
    }
}
?>


スタブメソッドをコールした結果として固定値 (returnValue() を参照ください) や (不変の) 引数 (returnArgument() を参照ください) ではなく計算した値を返したい場合は、 returnCallback() を使用します。 これは、スタブメソッドからコールバック関数やメソッドの結果を返させます。 例 9.7 を参照ください。

例 9.7: メソッドにコールバックからの値を返させるスタブ

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testReturnCallbackStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMock('SomeClass');

        // スタブの設定を行います
        $stub->expects($this->any())
             ->method('doSomething')
             ->will($this->returnCallback('str_rot13'));

        // $stub->doSomething($argument) は str_rot13($argument) を返します
        $this->assertEquals('fbzrguvat', $stub->doSomething('something'));
    }
}
?>


コールバックメソッドを設定するよりももう少しシンプルな方法として、 希望する返り値のリストを指定することもできます。この場合に使うのは onConsecutiveCalls() メソッドです。 例 9.8 の例を参照ください。

例 9.8: メソッドに、リストで指定した値をその順で返させるスタブ

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testOnConsecutiveCallsStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMock('SomeClass');

        // スタブの設定を行います
        $stub->expects($this->any())
             ->method('doSomething')
             ->will($this->onConsecutiveCalls(2, 3, 5, 7));

        // $stub->doSomething() は毎回異なる値を返します
        $this->assertEquals(2, $stub->doSomething());
        $this->assertEquals(3, $stub->doSomething());
        $this->assertEquals(5, $stub->doSomething());
    }
}
?>


値を返すのではなく、スタブメソッドで例外を発生させることもできます。 例 9.9 に、throwException() でこれを行う方法を示します。

例 9.9: メソッドに例外をスローさせるスタブ

<?php
require_once 'SomeClass.php';

class StubTest extends PHPUnit_Framework_TestCase
{
    public function testThrowExceptionStub()
    {
        // SomeClass クラスのスタブを作成します
        $stub = $this->getMock('SomeClass');

        // スタブの設定を行います
        $stub->expects($this->any())
             ->method('doSomething')
             ->will($this->throwException(new Exception));

        // $stub->doSomething() は例外をスローします
        $stub->doSomething();
    }
}
?>


また、スタブを使用することで、よりよい設計を行うことができるようにもなります。 あちこちで使用されているリソースを単一の窓口 (façade : ファサード) 経由でアクセスするようにすることで、 それを簡単にスタブに置き換えられるようになります。例えば、 データベースへのアクセスのコードをそこらじゅうにちりばめるのではなく、 その代わりに IDatabase インターフェイスを実装した単一の Database オブジェクトを使用するようにします。すると、 IDatabase を実装したスタブを作成することで、 それをテストに使用できるようになるのです。同時に、 テストを行う際にスタブデータベースを使用するか 本物のデータベースを使用するかを選択できるようになります。 つまり開発時にはローカル環境でテストし、 統合テスト時には実際のデータベースでテストするといったことができるようになるのです。

スタブ化しなければならない機能は、たいてい同一オブジェクト内で密結合しています。 この機能ををひとつの結合したインターフェイスにまとめることで、 システムのそれ以外の部分との結合を緩やかにすることができます。

モックオブジェクト

実際のオブジェクトを置き換えて、 (メソッドがコールされたことなどの) 期待する内容を検証するテストダブルのことを モック といいます。

モックオブジェクト は SUT の間接的な出力の内容を検証するために使用する観測地点です。 一般的に、モックオブジェクトにはテスト用スタブの機能も含まれます。 まだテストに失敗していない場合に、間接的な出力の検証用の値を SUT に返す機能です。 したがって、モックオブジェクトとは テスト用スタブにアサーション機能を足しただけのものとは異なります。 それ以外の用途にも使うことができます。

制限

そのテストのスコープ内で生成されたモックオブジェクトだけが、PHPUnit による自動検証の対象となります。 たとえばデータプロバイダなどで生成されたモックオブジェクトについては、PHPUnit では検証しません。

ひとつ例を示します。ここでは、別のオブジェクトを観察している あるオブジェクトの特定のメソッド (この例では update()) が正しくコールされたかどうかを調べるものとします。 例 9.10 は、テスト対象のシステム (SUT) の一部である Subject クラスと Observer クラスのコードです。

例 9.10: テスト対象のシステム (SUT) の一部である Subject クラスと Observer クラス

<?php
class Subject
{
    protected $observers = array();
    protected $name;
    
    public function __construct($name)
    {
        $this->name = $name;
    }
    
    public function getName()
    {
        return $this->name;
    }

    public function attach(Observer $observer)
    {
        $this->observers[] = $observer;
    }

    public function doSomething()
    {
        // なにかをします
        // ...

        // なにかしたということをオブザーバに通知します
        $this->notify('something');
    }

    public function doSomethingBad()
    {
        foreach ($this->observers as $observer) {
            $observer->reportError(42, 'Something bad happened', $this);
        }
    }

    protected function notify($argument)
    {
        foreach ($this->observers as $observer) {
            $observer->update($argument);
        }
    }

    // その他のメソッド
}

class Observer
{
    public function update($argument)
    {
        // なにかをします
    }

    public function reportError($errorCode, $errorMessage, Subject $subject)
    {
        // なにかをします
    }

    // その他のメソッド
}
?>


例 9.11 では、モックオブジェクトを作成して Subject オブジェクトと Observer オブジェクトの対話をテストする方法を説明します。

まず PHPUnit_Framework_TestCase クラスの getMock() メソッド を使用して Observer のモックオブジェクトを作成します。 getMock() メソッドの二番目の (オプションの) パラメータに配列を指定しているので、Observer クラスの中の update() メソッドについてのみモック実装が作成されます。

例 9.11: あるメソッドが、指定した引数で一度だけコールされることを確かめるテスト

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testObserversAreUpdated()
    {
        // Observer クラスのモックを作成します。
        // update() メソッドのみのモックです。
        $observer = $this->getMock('Observer', array('update'));

        // update() メソッドが一度だけコールされ、その際の
        // パラメータは文字列 'something' となる、
        // ということを期待しています。
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));

        // Subject オブジェクトを作成し、Observer オブジェクトの
        // モックをアタッチします。
        $subject = new Subject('My subject');
        $subject->attach($observer);

        // $subject オブジェクトの doSomething() メソッドをコールします。
        // これは、Observer オブジェクトのモックの update() メソッドを、
        // 文字列 'something' を引数としてコールすることを期待されています。
        $subject->doSomething();
    }
}
?>


with() メソッドには任意の数の引数を渡すことができます。 これは、モック対象のメソッドの引数の数に対応します。 メソッドの引数に対して、単なるマッチだけでなくより高度な制約を指定することもできます。

例 9.12: メソッドが引数つきでコールされることを、さまざまな制約の下でテストする例

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testErrorReported()
    {
        // Observer クラスのモックを作成します。
        // reportError() メソッドをモックします。
        $observer = $this->getMock('Observer', array('reportError'));

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with($this->greaterThan(0),
                        $this->stringContains('Something'),
                        $this->anything());

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() メソッドは、
        // reportError() メソッドを通じてオブザーバにエラーを報告しなければなりません。
        $subject->doSomethingBad();
    }
}
?>


callback() 制約を使えば、より複雑な引数の検証ができます。 この制約は、PHP のコールバックを引数として受け取ります。 このコールバックは、検証したい引数を受け取って、検証を通過した場合に TRUE、 それ以外の場合に FALSE を返します。

例 9.13: より複雑な引数の検証

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testErrorReported()
    {
        // Observer クラスのモックを作成します。
        // reportError() メソッドをモックします。
        $observer = $this->getMock('Observer', array('reportError'));

        $observer->expects($this->once())
                 ->method('reportError')
                 ->with($this->greaterThan(0),
                        $this->stringContains('Something'),
                        $this->callback(function($subject){
                          return is_callable(array($subject, 'getName')) &&
                                 $subject->getName() == 'My subject';
                        }));

        $subject = new Subject('My subject');
        $subject->attach($observer);

        // doSomethingBad() メソッドは、
        // reportError() メソッドを通じてオブザーバにエラーを報告しなければなりません。
        $subject->doSomethingBad();
    }
}
?>


例 9.14: メソッドが一度だけ呼ばれ、同じオブジェクトが渡されたことを確かめるテスト

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testIdenticalObjectPassed()
    {
        $expectedObject = new stdClass;

        $mock = $this->getMock('stdClass', array('foo'));
        $mock->expects($this->once())
             ->method('foo')
             ->with($this->identicalTo($expectedObject));

        $mock->foo($expectedObject);
    }
}
?>


例 9.15: パラメータのクローンの有効にしたモックオブジェクトの作成

<?php
class FooTest extends PHPUnit_Framework_TestCase
{
    public function testIdenticalObjectPassed()
    {
        $cloneArguments = true;

        $mock = $this->getMock(
            'stdClass',
            array(),
            array(),
            '',
            FALSE,
            TRUE,
            TRUE,
            $cloneArguments
        );

        // あるいは、モックビルダーを使います
        $mock = $this->getMockBuilder('stdClass')
                     ->enableArgumentCloning()
                     ->getMock();

        // これでモックがパラメータをクローンするようになり、
        // identicalTo 制約は失敗します
    }
}
?>


表 2.3 はメソッドの引数に適用できる制約、そして 表 9.1 は起動回数を指定するために使える matcher です。

表9.1 Matchers

Matcher意味
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any()評価対象のメソッドがゼロ回以上実行された際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedCount never()評価対象のメソッドが実行されなかった際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce()評価対象のメソッドが最低一回以上実行された際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedCount once()評価対象のメソッドが一度だけ実行された際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count)評価対象のメソッドが指定した回数だけ実行された際にマッチするオブジェクトを返します。
PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index)評価対象のメソッドが $index 回目に実行された際にマッチするオブジェクトを返します。


注記

at() マッチャーのパラメータ $index は、 指定したモックオブジェクトでの すべてのメソッドの実行 の、ゼロからはじまるインデックスを参照します。 このマッチャーを使うときには注意しましょう。テストが実装の詳細とあまりにも密結合になり、 脆いテストになってしまう可能性があるからです。

トレイトと抽象クラスのモック

getMockForTrait() メソッドは、指定したトレイトを使ったモックオブジェクトを返します。 そのトレイトのすべての抽象メソッドがモックの対象となります。 これを使えば、トレイトの具象メソッドをテストすることができます。

例 9.16: トレイトの具象メソッドのテスト

<?php
trait AbstractTrait
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class TraitClassTest extends PHPUnit_Framework_TestCase
{
    public function testConcreteMethod()
    {
        $mock = $this->getMockForTrait('AbstractTrait');
        $mock->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(TRUE));

        $this->assertTrue($mock->concreteMethod());
    }
}
?>


getMockForAbstractClass() メソッドは、 抽象クラスのモックオブジェクトを返します。 そのクラスのすべての抽象メソッドがモックの対象となります。 これを使えば、抽象クラスにある具象メソッドをテストすることができます。

例 9.17: 抽象クラスの具象メソッドのテスト

<?php
abstract class AbstractClass
{
    public function concreteMethod()
    {
        return $this->abstractMethod();
    }

    public abstract function abstractMethod();
}

class AbstractClassTest extends PHPUnit_Framework_TestCase
{
    public function testConcreteMethod()
    {
        $stub = $this->getMockForAbstractClass('AbstractClass');
        $stub->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(TRUE));

        $this->assertTrue($stub->concreteMethod());
    }
}
?>


ウェブサービスのスタブおよびモック

ウェブサービスとのやりとりを行うアプリケーションを、 実際にウェブサービスとやりとりすることなくテストしたくなることもあるでしょう。 ウェブサービスのスタブやモックを作りやすくするために getMockFromWsdl() メソッドが用意されており、これは getMock() (上を参照ください) とほぼ同様に使うことができます。唯一の違いは、 getMockFromWsdl() が返すスタブやモックが WSDL のウェブサービス記述にもとづくものであるのに対して getMock() が返すスタブやモックが PHP のクラスやインターフェイスにもとづくものであるという点です。

例 9.18 は、getMockFromWsdl() を使って GoogleSearch.wsdl に記述されたウェブサービスのスタブを作る例です。

例 9.18: ウェブサービスのスタブ

<?php
class GoogleTest extends PHPUnit_Framework_TestCase
{
    public function testSearch()
    {
        $googleSearch = $this->getMockFromWsdl(
          'GoogleSearch.wsdl', 'GoogleSearch'
        );

        $directoryCategory = new StdClass;
        $directoryCategory->fullViewableName = '';
        $directoryCategory->specialEncoding = '';

        $element = new StdClass;
        $element->summary = '';
        $element->URL = 'http://www.phpunit.de/';
        $element->snippet = '...';
        $element->title = '<b>PHPUnit</b>';
        $element->cachedSize = '11k';
        $element->relatedInformationPresent = TRUE;
        $element->hostName = 'www.phpunit.de';
        $element->directoryCategory = $directoryCategory;
        $element->directoryTitle = '';

        $result = new StdClass;
        $result->documentFiltering = FALSE;
        $result->searchComments = '';
        $result->estimatedTotalResultsCount = 378000;
        $result->estimateIsExact = FALSE;
        $result->resultElements = array($element);
        $result->searchQuery = 'PHPUnit';
        $result->startIndex = 1;
        $result->endIndex = 1;
        $result->searchTips = '';
        $result->directoryCategories = array();
        $result->searchTime = 0.248822;

        $googleSearch->expects($this->any())
                     ->method('doGoogleSearch')
                     ->will($this->returnValue($result));

        /**
         * $googleSearch->doGoogleSearch() はスタブが用意した結果を返し、
         * ウェブサービスの doGoogleSearch() が呼び出されることはありません
         */
        $this->assertEquals(
          $result,
          $googleSearch->doGoogleSearch(
            '00000000000000000000000000000000',
            'PHPUnit',
            0,
            1,
            FALSE,
            '',
            FALSE,
            '',
            '',
            ''
          )
        );
    }
}
?>


ファイルシステムのモック

vfsStream仮想ファイルシステム 用の ストリームラッパー で、 ユニットテストにおいて実際のファイルシステムのモックを作るときに有用です。

vfsStream をインストールするには、配布元の PEAR チャンネル (pear.bovigo.org) をローカルの PEAR 環境に登録しなければなりません。

pear channel-discover pear.bovigo.org

これが必要なのは最初の一度だけです。これで、 PEAR インストーラを使って vfsStream をインストールできるようになりました。

pear install bovigo/vfsStream-beta

例 9.19 は、ファイルシステムを操作するクラスの例です。

例 9.19: ファイルシステムを操作するクラス

<?php
class Example
{
    protected $id;
    protected $directory;

    public function __construct($id)
    {
        $this->id = $id;
    }

    public function setDirectory($directory)
    {
        $this->directory = $directory . DIRECTORY_SEPARATOR . $this->id;

        if (!file_exists($this->directory)) {
            mkdir($this->directory, 0700, TRUE);
        }
    }
}?>


vfsStream のような仮想ファイルシステムがなければ、外部への影響なしに setDirectory() メソッドを個別にテストすることができません (例 9.20 を参照ください)。

例 9.20: ファイルシステムを操作するクラスのテスト

<?php
require_once 'Example.php';

class ExampleTest extends PHPUnit_Framework_TestCase
{
    protected function setUp()
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(file_exists(dirname(__FILE__) . '/id'));

        $example->setDirectory(dirname(__FILE__));
        $this->assertTrue(file_exists(dirname(__FILE__) . '/id'));
    }

    protected function tearDown()
    {
        if (file_exists(dirname(__FILE__) . '/id')) {
            rmdir(dirname(__FILE__) . '/id');
        }
    }
}
?>


この方式には、次のような問題があります。

  • 外部のリソースを使うため、ファイルシステムのテストが断続的になる可能性があります。その結果、テストがあまり当てにならないものになります。

  • setUp()tearDown() で、テストの前後にそのディレクトリがないことを確認する必要があります。

  • tearDown() メソッドを実行する前にテストが異常終了したときに、ファイルシステム上にディレクトリが残ったままとなります。

例 9.21 は、vfsStream を使ってファイルシステムのモックを作成し、 ファイルシステムを操作するクラスのテストを行う例です。

例 9.21: ファイルシステムを操作するクラスのテストにおけるファイルシステムのモックの作成

<?php
require_once 'vfsStream/vfsStream.php';
require_once 'Example.php';

class ExampleTest extends PHPUnit_Framework_TestCase
{
    public function setUp()
    {
        vfsStreamWrapper::register();
        vfsStreamWrapper::setRoot(new vfsStreamDirectory('exampleDir'));
    }

    public function testDirectoryIsCreated()
    {
        $example = new Example('id');
        $this->assertFalse(vfsStreamWrapper::getRoot()->hasChild('id'));

        $example->setDirectory(vfsStream::url('exampleDir'));
        $this->assertTrue(vfsStreamWrapper::getRoot()->hasChild('id'));
    }
}
?>


この方式には次のような利点があります。

  • テストが簡潔になります。

  • vfsStream が、テスト対象のコードから操作するファイルシステム環境を用意してくれるので、開発者はそれを自由に扱えるようになります。

  • 実際のファイルシステムを操作することがなくなるので、tearDown() メソッドでの後始末が不要になります。

このページの改善案をGitHubで提案してください!