第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 の createMock($type) メソッドや getMockBuilder($type) メソッドを使うと、 指定した元インターフェイス (あるいは元クラス) のテストダブルとして振る舞うオブジェクトを自動的に生成することができます。 このテストダブルオブジェクトは、元のオブジェクトを要するすべての場面で使うことができます。

createMock($type) メソッドは、指定した型 (インターフェイスやクラス) のテストダブルオブジェクトをその場で返します。 テストダブルの作成は、デフォルトではベストプラクティスに沿って行われます (元クラスの __construct()__clone() は実行しません)。また、テストダブルのメソッドに渡された引数はクローンされません。 デフォルトと異なる挙動を求める場合は、 getMockBuilder($type) メソッドを用いてテストダブルの生成処理をカスタマイズする必要があります。

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

制限:final、private および static メソッド

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

スタブ

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

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

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

<?php
use PHPUnit\Framework\TestCase;

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

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

<?php
use PHPUnit\Framework\TestCase;

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

        // スタブの設定を行います
        $stub->method('doSomething')
             ->willReturn('foo');

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

制限:"method" という名前のメソッド

この例がきちんと動作するのは、元のクラスで "method" という名前のメソッドが宣言されていない場合だけです。

元のクラスで "method" という名前のメソッドが宣言されている場合は、 $stub->expects($this->any())->method('doSomething')->willReturn('foo'); としなければいけません。

舞台裏では、createMock() メソッドが使われたときに PHPUnit が自動的に、求める振る舞いを実装した新たな PHP のクラスを生成しています。

例 9.3 に例を示します。 これは、モックビルダーの流れるようなインターフェイスを使って、テストダブルの作成方法を設定するものです。 このテストダブルで使っている設定は、createMock() がデフォルトで使用するベストプラクティスと同じです。

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

<?php
use PHPUnit\Framework\TestCase;

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

        // スタブの設定を行います
        $stub->method('doSomething')
             ->willReturn('foo');

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

ここまでの例では、 willReturn($value) を使ってシンプルな値を返していました。 この構文は、 will($this->returnValue($value)) と同じ意味です。 この長い構文での検証を使うと、より複雑な動きをするスタブも作れます。

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

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

<?php
use PHPUnit\Framework\TestCase;

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

        // スタブの設定を行います
        $stub->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
use PHPUnit\Framework\TestCase;

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

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

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

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

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

<?php
use PHPUnit\Framework\TestCase;

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

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

        // スタブの設定を行います
        $stub->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
use PHPUnit\Framework\TestCase;

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

        // スタブの設定を行います
        $stub->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
use PHPUnit\Framework\TestCase;

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

        // スタブの設定を行います
        $stub->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
use PHPUnit\Framework\TestCase;

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

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

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

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

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

モックオブジェクト

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

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

制限:期待値の自動検証

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

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

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

<?php
use PHPUnit\Framework\TestCase;

class Subject
{
    protected $observers = [];
    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 クラスの getMockBuilder() メソッドを使用して Observer のモックオブジェクトを作成します。 getMock() メソッドの二番目の (オプションの) パラメータに配列を指定しているので、Observer クラスの中の update() メソッドについてのみモック実装が作成されます。

あるメソッドがコールされたのかどうか、そしてどんな引数を渡してコールされたのかを検証したいので、 expects() メソッドと with() メソッドを用意しました。 これらを使って、このやりとりがどのように行われるのかを指定します。

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

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testObserversAreUpdated()
    {
        // Observer クラスのモックを作成します。
        // update() メソッドのみのモックです。
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['update'])
                         ->getMock();

        // 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
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testErrorReported()
    {
        // Observer クラスのモックを作成します。
        // reportError() メソッドをモックします。
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['reportError'])
                         ->getMock();

        $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();
    }
}
?>

withConsecutive() メソッドには、 テスト対象の呼び出しにあわせて、引数の配列を好きなだけ渡せます。 個々の配列は制約のリストです。 with() と同様に、これがモック対象メソッドのそれぞれの引数に対応します。

例 9.13: あるメソッドが、指定した引数つきで 2 回呼び出されることを確かめるテスト

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testFunctionCalledTwoTimesWithSpecificArguments()
    {
        $mock = $this->getMockBuilder(stdClass::class)
                     ->setMethods(['set'])
                     ->getMock();

        $mock->expects($this->exactly(2))
             ->method('set')
             ->withConsecutive(
                 [$this->equalTo('foo'), $this->greaterThan(0)],
                 [$this->equalTo('bar'), $this->greaterThan(0)]
             );

        $mock->set('foo', 21);
        $mock->set('bar', 48);
    }
}
?>

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

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

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testErrorReported()
    {
        // Observer クラスのモックを作成します。
        // reportError() メソッドをモックします。
        $observer = $this->getMockBuilder(Observer::class)
                         ->setMethods(['reportError'])
                         ->getMock();

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

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

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

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

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testIdenticalObjectPassed()
    {
        $expectedObject = new stdClass;

        $mock = $this->getMockBuilder(stdClass::class)
                     ->setMethods(['foo'])
                     ->getMock();

        $mock->expects($this->once())
             ->method('foo')
             ->with($this->identicalTo($expectedObject));

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

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

<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testIdenticalObjectPassed()
    {
        $cloneArguments = true;

        $mock = $this->getMockBuilder(stdClass::class)
                     ->enableArgumentCloning()
                     ->getMock();

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

表 A.1 はメソッドの引数に適用できる制約、そして 表 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 は、 指定したモックオブジェクトでの すべてのメソッドの実行 の、ゼロからはじまるインデックスを参照します。 このマッチャーを使うときには注意しましょう。テストが実装の詳細とあまりにも密結合になり、 脆いテストになってしまう可能性があるからです。

最初に説明したとおり、createMock() メソッドが用いるデフォルトのテストダブル生成方法がニーズを満たさない場合は、 getMockBuilder($type) メソッドを使えば生成方法をカスタマイズできます。 モックビルダーが提供するメソッドの一覧は、次のとおりです。

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

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

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

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

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

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

Prophecy

Prophecy は 「クセは強いけれども、強力で柔軟な、PHP のオブジェクトモッキングフレームワークです。 最初は phpspec2 のニーズを満たすために作られましたが、今やそれ以外のテスティングフレームワークでも、 最小限の努力で使えるようになりました」 とのことです。

PHPUnit は、Prophecy を使ったテストダブルの作成に標準で対応しています。 例 9.17 は、例 9.11 と同じテストを、Prophecy の理念に沿って表すとどうなるかを示す例です。

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

<?php
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function testObserversAreUpdated()
    {
        $subject = new Subject('My subject');

        // Observer クラスの prophecy を作成します。
        $observer = $this->prophesize(Observer::class);

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

        // prophecy を公開し、モックオブジェクトを
        // Subject にアタッチします。
        $subject->attach($observer->reveal());

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

Prophecy を使ってスタブやスパイそしてモックを作ったり設定したり使ったりする方法の詳細については、 その ドキュメント を参照ください。

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

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

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

<?php
use PHPUnit\Framework\TestCase;

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

    public abstract function abstractMethod();
}

class TraitClassTest extends TestCase
{
    public function testConcreteMethod()
    {
        $mock = $this->getMockForTrait(AbstractTrait::class);

        $mock->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(true));

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

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

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

<?php
use PHPUnit\Framework\TestCase;

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

    public abstract function abstractMethod();
}

class AbstractClassTest extends TestCase
{
    public function testConcreteMethod()
    {
        $stub = $this->getMockForAbstractClass(AbstractClass::class);

        $stub->expects($this->any())
             ->method('abstractMethod')
             ->will($this->returnValue(true));

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

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

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

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

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

<?php
use PHPUnit\Framework\TestCase;

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

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

        $element = new stdClass;
        $element->summary = '';
        $element->URL = 'https://phpunit.de/';
        $element->snippet = '...';
        $element->title = '<b>PHPUnit</b>';
        $element->cachedSize = '11k';
        $element->relatedInformationPresent = true;
        $element->hostName = 'phpunit.de';
        $element->directoryCategory = $directoryCategory;
        $element->directoryTitle = '';

        $result = new stdClass;
        $result->documentFiltering = false;
        $result->searchComments = '';
        $result->estimatedTotalResultsCount = 3.9000;
        $result->estimateIsExact = false;
        $result->resultElements = [$element];
        $result->searchQuery = 'PHPUnit';
        $result->startIndex = 1;
        $result->endIndex = 1;
        $result->searchTips = '';
        $result->directoryCategories = [];
        $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仮想ファイルシステム 用の ストリームラッパー で、 ユニットテストにおいて実際のファイルシステムのモックを作るときに有用です。

Composer を使ってプロジェクトの依存関係を管理するには、 mikey179/vfsStream への依存情報をプロジェクトの composer.json ファイルに追加します。 次に示すのは最小限の composer.json ファイルの例で、 開発時の PHPUnit 4.6 と vfsStream への依存を定義しています。

{
    "require-dev": {
        "phpunit/phpunit": "~4.6",
        "mikey179/vfsStream": "~1"
    }
}

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

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

<?php
use PHPUnit\Framework\TestCase;

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.22 を参照ください)。

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

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends 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.23 は、vfsStream を使ってファイルシステムのモックを作成し、 ファイルシステムを操作するクラスのテストを行う例です。

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

<?php
use PHPUnit\Framework\TestCase;

class ExampleTest extends 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() メソッドでの後始末が不要になります。