こんにちは。よっしーです(^^)
今日は、PHPUnitにおけるテストスタブについてご紹介します。
背景
ジェラルド・メザロスは、彼の「xUnit Test Patterns」の書籍で、次のようにしてテストダブルの概念を紹介しています。
システムアンダーテスト(SUT)をテストすることが、単純に難しい場合があります。なぜなら、テスト環境では使用できない他のコンポーネントに依存しているためかもしれません。これは、利用できない、必要な結果を返さない、または実行すると望ましくない副作用があるためかもしれません。また、テスト戦略がSUTの内部動作により多くの制御や可視性を必要とする場合もあります。
テストを書いている場合、実際の依存コンポーネント(DOC)を使用できない(または使用しないことを選択した)場合、テストダブルでそれを置き換えることができます。テストダブルは、実際のDOCとまったく同じように振る舞う必要はありません。単にSUTが実際のDOCと同じAPIを提供していると思えれば良いのです。
createStub(string $type)とcreateMock(string $type)メソッドは、テスト内で使用することで、指定された元の型(インターフェースまたは拡張可能なクラス)のテストダブルとして自動生成されるオブジェクトを生成するのに使用できます。このテストダブルオブジェクトは、元の型のオブジェクトが期待または必要とされるすべてのコンテキストで使用できます。
テストスタブ
オブジェクトをテストダブルで置き換えて、(必要に応じて)設定された戻り値を返す方法は、スタブ化(Stubbing)として知られています。テストスタブを使用することで、「テストがSUTの間接入力に対して制御ポイントを持つように、SUTが依存している実際のコンポーネントを置き換えます。これにより、テストはSUTを他には実行しない可能性のあるパスに強制することができます」(ジェラルド・メザロス)と説明されています。
テストスタブの作成
createStub()
createStub(string $type)メソッドは、指定されたインターフェースまたは拡張可能なクラスに対してテストスタブを返します。
元の型のすべてのメソッドは、元のメソッドを呼び出さずに、メソッドの戻り値の型宣言を満たす自動生成された値を返す実装で置き換えられます。これらのメソッドは「ダブルされたメソッド」と呼ばれます。
ダブルされたメソッドの動作は、willReturn()やwillThrowException()などのメソッドを使用して構成することができます。これらのメソッドは後で説明します。
createStubForIntersectionOfInterfaces()
createStubForIntersectionOfInterfaces(array $interfaces) メソッドを使用すると、 インターフェイス名のリストに基づいてインターフェイスの交差用のテストスタブを作成することができます。
次のようなインターフェイス X および Y があるとしましょう。
<?php declare(strict_types=1);
interface X
{
public function m(): bool;
}
<?php declare(strict_types=1);
interface Y
{
public function n(): int;
}
Zという名前のテストしたいクラスがある。
<?php declare(strict_types=1);
final class Z
{
public function doSomething(X&Y $input): bool
{
$result = false;
// ...
return $result;
}
}
Z をテストするには、X&Y の交差型を満たすオブジェクトが必要です。createStubForIntersectionOfInterfaces(array $interfaces) メソッドを使用して、 X&Y を満たすテストスタブを作成します。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class StubForIntersectionExampleTest extends TestCase
{
public function testCreateStubForIntersection(): void
{
$o = $this->createStubForIntersectionOfInterfaces([X::class, Y::class]);
// $o is of type X ...
$this->assertInstanceOf(X::class, $o);
// ... and $o is of type Y
$this->assertInstanceOf(Y::class, $o);
}
}
createConfiguredStub()
createConfiguredStub() メソッドは createStub() の便利なラッパーで、連想配列 ([‘methodName’ => <返り値>]) を使用して返り値を設定できます。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class CreateConfiguredStubExampleTest extends TestCase
{
public function testCreateConfiguredStub(): void
{
$o = $this->createConfiguredStub(
SomeInterface::class,
[
'doSomething' => 'foo',
'doSomethingElse' => 'bar',
]
);
// $o->doSomething() now returns 'foo'
$this->assertSame('foo', $o->doSomething());
// $o->doSomethingElse() now returns 'bar'
$this->assertSame('bar', $o->doSomethingElse());
}
}
テストスタブの設定
willReturn()
たとえば、willReturn() メソッドを使用すると、呼び出されたときに指定した値を返すように倍加メソッドを構成できます。この設定された値は、メソッドの戻り値の型宣言と互換性がなければなりません。
テストしたいクラス SomeClass があるとします。
<?php declare(strict_types=1);
final class SomeClass
{
public function doSomething(Dependency $dependency): string
{
$result = '';
// ...
return $result . $dependency->doSomething();
}
}
<?php declare(strict_types=1);
interface Dependency
{
public function doSomething(): string;
}
ここでは、createStub(string $type) メソッドを使用して Dependency のテストスタブを作成し、 実際の Dependency の実装を使わずに SomeClass をテストする方法を示します。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class SomeClassTest extends TestCase
{
public function testDoesSomething(): void
{
$sut = new SomeClass;
// Create a test stub for the Dependency interface
$dependency = $this->createStub(Dependency::class);
// Configure the test stub
$dependency->method('doSomething')
->willReturn('foo');
$result = $sut->doSomething($dependency);
$this->assertStringEndsWith('foo', $result);
}
}
上記の例では、まずcreateStub()メソッドを使用してテストスタブを作成します。これは、Dependencyのインスタンスのように見えるオブジェクトです。
その後、PHPUnitが提供するFluent Interfaceを使用して、テストスタブの振る舞いを指定します。
「裏側」で、PHPUnitはcreateStub()メソッドを使用すると、必要な振る舞いを実装する新しいPHPクラスを自動生成します。
createStub()は、メソッドの戻り値の型に基づいて自動的に再帰的に戻り値をスタブ化します。以下に示す例を考えてみてください。
<?php declare(strict_types=1);
class C
{
public function m(): D
{
// Do something.
}
}
上記の例では、C::m()メソッドには戻り値の型宣言があり、このメソッドがD型のオブジェクトを返すことを示しています。Cのテストダブルが作成され、m()に対してwillReturn()を使用して戻り値が設定されていない場合、m()が呼び出されたときにPHPUnitは自動的にDのテストダブルを作成して返します。
同様に、mがスカラー型の戻り値宣言を持っている場合、intの場合は0、floatの場合は0.0、arrayの場合は[]などの戻り値が生成されます。
希望する複数の戻り値を指定することもできます。以下はその例です。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class OnConsecutiveCallsExampleTest extends TestCase
{
public function testOnConsecutiveCallsStub(): void
{
// Create a stub for the SomeClass class.
$stub = $this->createStub(SomeClass::class);
// Configure the stub.
$stub->method('doSomething')
->willReturn(1, 2, 3);
// $stub->doSomething() returns a different value each time
$this->assertSame(1, $stub->doSomething());
$this->assertSame(2, $stub->doSomething());
$this->assertSame(3, $stub->doSomething());
}
}
willThrowException()
値を返す代わりに、スタブメソッドは例外を発生させることもできます。そのために willThrowException() を使う例を示します。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ThrowExceptionExampleTest extends TestCase
{
public function testThrowExceptionStub(): void
{
// Create a stub for the SomeClass class.
$stub = $this->createStub(SomeClass::class);
// Configure the stub.
$stub->method('doSomething')
->willThrowException(new Exception);
// $stub->doSomething() throws Exception
$stub->doSomething();
}
}
willReturnArgument()
スタブされたメソッド呼び出しの結果として、メソッド呼び出しの引数のひとつを(変更せずに)返したいことがあります。以下は、willReturnArgument() を使用してこれを実現する方法を示す例です。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ReturnArgumentExampleTest extends TestCase
{
public function testReturnArgumentStub(): void
{
// Create a stub for the SomeClass class.
$stub = $this->createStub(SomeClass::class);
// Configure the stub.
$stub->method('doSomething')
->willReturnArgument(0);
// $stub->doSomething('foo') returns 'foo'
$this->assertSame('foo', $stub->doSomething('foo'));
// $stub->doSomething('bar') returns 'bar'
$this->assertSame('bar', $stub->doSomething('bar'));
}
}
willReturnCallback()
スタブメソッド呼び出しが、固定値 (willReturn() を参照) や (変更されていない) 引数 (willReturnArgument() を参照) ではなく、計算された値を返す必要がある場合、 willReturnCallback() を使用して、スタブメソッドがコールバック関数やメソッドの結果を返すようにすることができます。以下に例を示します。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ReturnCallbackExampleTest extends TestCase
{
public function testReturnCallbackStub(): void
{
// Create a stub for the SomeClass class.
$stub = $this->createStub(SomeClass::class);
// Configure the stub.
$stub->method('doSomething')
->willReturnCallback('str_rot13');
// $stub->doSomething($argument) returns str_rot13($argument)
$this->assertSame('fbzrguvat', $stub->doSomething('something'));
}
}
willReturnSelf()
流暢なインターフェイスをテストするとき、スタブされたメソッドがスタブされたオブジェクトへの参照を返すようにすると便利なことがあります。ここでは、willReturnSelf() を使ってこれを実現する例を示します。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ReturnSelfExampleTest extends TestCase
{
public function testReturnSelf(): void
{
// Create a stub for the SomeClass class.
$stub = $this->createStub(SomeClass::class);
// Configure the stub.
$stub->method('doSomething')
->willReturnSelf();
// $stub->doSomething() returns $stub
$this->assertSame($stub, $stub->doSomething());
}
}
willReturnValueMap()
スタブされたメソッドが、あらかじめ定義された引数のリストによって異なる値を返すことがあります。ここでは、willReturnValueMap() を使用して、引数と対応する返り値を関連付けるマップを作成する例を示します。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class ReturnValueMapExampleTest extends TestCase
{
public function testReturnValueMapStub(): void
{
// Create a stub for the SomeClass class.
$stub = $this->createStub(SomeClass::class);
// Create a map of arguments to return values.
$map = [
['a', 'b', 'c', 'd'],
['e', 'f', 'g', 'h'],
];
// Configure the stub.
$stub->method('doSomething')
->willReturnValueMap($map);
// $stub->doSomething() returns different values depending on
// the provided arguments.
$this->assertSame('d', $stub->doSomething('a', 'b', 'c'));
$this->assertSame('h', $stub->doSomething('e', 'f', 'g'));
}
}
おわりに
PHPUnitにおけるテストスタブについてご紹介しました。次回は、Mockについてご紹介したいと思います。
何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント