こんにちは。よっしーです(^^)
今日は、PHPUnitにおけるモックについてご紹介します。
- 背景
- モック・オブジェクトの作成
- モック・オブジェクトの設定
- モックビルダー API
- setMockClassName()
- setConstructorArgs()
- disableOriginalConstructor()
- enableOriginalConstructor()
- disableOriginalClone()
- enableOriginalClone()
- enableArgumentCloning()
- disableArgumentCloning()
- disableAutoReturnValueGeneration()
- enableAutoReturnValueGeneration()
- disallowMockingUnknownTypes()
- allowMockingUnknownTypes()
- disableAutoload()
- enableAutoload()
- enableProxyingToOriginalMethods()
- disableProxyingToOriginalMethods()
- onlyMethods()
- addMethods()
- getMock()
- getMockForAbstractClass()
- getMockForTrait()
- スタブとモックの違い
- おわりに
背景
物体をテストのダブルに置き換えて、例えばメソッドが呼び出されたことを検証するなど、期待を確認するテクニックを「モッキング」と呼びます。
モックオブジェクトは、「システムアンダーテストの間接的な出力を検証するために使用される観察点」として使うことができます。通常、モックオブジェクトはテストスタブの機能も備えており、テストが失敗していなければシステムアンダーテストに値を返す必要がありますが、重点は間接的な出力の検証にあります。したがって、モックオブジェクトは単なるテストスタブとアサーションだけで構成されるものではなく、根本的に異なる方法で使用されます。(Gerard Meszaros)
モック・オブジェクトの作成
createMock()
createMock(string $type) メソッドは、指定されたインターフェースまたは拡張可能なクラスに対してモックオブジェクトを返します。
元のタイプのすべてのメソッドは、元のメソッドを呼び出さずに、メソッドの戻り値の型宣言を満たす自動生成された値を返す実装に置き換えられます。これらのメソッドは「ダブルドメソッド」と呼ばれます。
ダブルドメソッドの動作は、willReturn() や willThrowException() などのメソッドを使用して設定することができます。これらのメソッドについては、前回の記事にテストスタブのセクションで説明されています。
ダブルドメソッドの呼び出しに対する期待(「指定された引数でメソッドが呼び出される必要がある」、「メソッドは呼び出されない必要がある」など)は、モックオブジェクトの expects() メソッドを使用して設定することができます。
createMockForIntersectionOfInterfaces()
createMockForIntersectionOfInterfaces(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 の共通部分のインターフェースを満たすオブジェクトが必要です。createMockForIntersectionOfInterfaces(array $interfaces) メソッドを使用して、以下のように X と Y の両方を満たすテストモックを作成できます。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class MockForIntersectionExampleTest extends TestCase
{
public function testCreateMockForIntersection(): void
{
$o = $this->createMockForIntersectionOfInterfaces([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);
}
}
createConfiguredMock()
createConfiguredMock() メソッドは、createMock() をより便利に使用できるようにしたもので、連想配列([‘methodName’ => <return value>])を使用して戻り値を設定することができます。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class CreateConfiguredMockExampleTest extends TestCase
{
public function testCreateConfiguredMock(): void
{
$o = $this->createConfiguredMock(
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());
}
}
getMockForAbstractClass()
getMockForAbstractClass() メソッドは、抽象クラスに対するモックオブジェクトを返します。指定された抽象クラスのすべての抽象メソッドがモック化されます。これにより、抽象クラスの具象メソッドをテストすることができます。
<?php declare(strict_types=1);
abstract class AbstractClass
{
public function concreteMethod()
{
return $this->abstractMethod();
}
abstract public function abstractMethod();
}
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class AbstractClassTest extends TestCase
{
public function testConcreteMethod(): void
{
$stub = $this->getMockForAbstractClass(AbstractClass::class);
$stub->expects($this->any())
->method('abstractMethod')
->willReturn(true);
$this->assertTrue($stub->concreteMethod());
}
}
getMockForTrait()
getMockForTrait() メソッドは、指定された trait を使用するモック・オブジェクトを返します。指定された trait のすべての抽象メソッドがモックされます。これにより、 trait の具象メソッドをテストすることができます。
<?php declare(strict_types=1);
trait AbstractTrait
{
public function concreteMethod()
{
return $this->abstractMethod();
}
abstract public function abstractMethod();
}
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class AbstractTraitTest extends TestCase
{
public function testConcreteMethod(): void
{
$mock = $this->getMockForTrait(AbstractTrait::class);
$mock->expects($this->any())
->method('abstractMethod')
->willReturn(true);
$this->assertTrue($mock->concreteMethod());
}
}
getMockFromWsdl()
アプリケーションがウェブサービスとやり取りする場合、実際にウェブサービスとやり取りせずにテストしたいことがあります。ウェブサービスのスタブやモックを作成するために、getMockFromWsdl() メソッドが使用できます。
このメソッドは、WSDLで記述されたウェブサービスの説明に基づいてモックオブジェクトを返します。一方、createMock() メソッドはインターフェースまたはクラスに基づいてモックオブジェクトを返します。
以下は、HelloService.wsdl で記述されたウェブサービスをスタブ化する例です。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class WsdlStubExampleTest extends TestCase
{
public function testWebserviceCanBeStubbed(): void
{
$service = $this->getMockFromWsdl(__DIR__ . '/HelloService.wsdl');
$service->method('sayHello')
->willReturn('Hello');
$this->assertSame('Hello', $service->sayHello('message'));
}
}
モック・オブジェクトの設定
次のような場合を考えてみましょう。別のオブジェクトを観察するオブジェクトに対して正しいメソッド(例えば update())が呼び出されるかをテストしたいとします。
以下は、システムアンダーテスト(SUT)に含まれる Subject クラスと Observer インターフェースのコードです:
<?php declare(strict_types=1);
final class Subject
{
private array $observers = [];
public function attach(Observer $observer): void
{
$this->observers[] = $observer;
}
public function doSomething(): void
{
// ...
$this->notify('something');
}
private function notify(string $argument): void
{
foreach ($this->observers as $observer) {
$observer->update($argument);
}
}
// ...
}
<?php declare(strict_types=1);
interface Observer
{
public function update(string $argument): void;
}
以下は、モック・オブジェクトを使用して、SubjectオブジェクトとObserverオブジェクトの相互作用をテストする方法を示す例です。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class SubjectTest extends TestCase
{
public function testObserversAreUpdated(): void
{
$observer = $this->createMock(Observer::class);
$observer->expects($this->once())
->method('update')
->with($this->identicalTo('something'));
$subject = new Subject;
$subject->attach($observer);
$subject->doSomething();
}
}
最初に createMock() メソッドを使用して Observer のモックオブジェクトを作成します。
二つのオブジェクト間の通信を検証することが目的なので、expects() メソッドと with() メソッドを使用して、この通信の形式を指定します。
with() メソッドは、モック化されるメソッドの引数の数に応じて任意の数の引数を取ることができます。単純な一致だけでなく、メソッドの引数に対してより高度な制約を指定することもできます。
Constraints には、メソッド引数に適用できる制約が示されており、以下は呼び出し回数を指定するために使用できるマッチャーのリストです:
- any() は、対象のメソッドが0回以上実行される場合に一致するマッチャーを返します。
- never() は、対象のメソッドが実行されない場合に一致するマッチャーを返します。
- atLeastOnce() は、対象のメソッドが少なくとも1回実行される場合に一致するマッチャーを返します。
- once() は、対象のメソッドが正確に1回実行される場合に一致するマッチャーを返します。
- atMost(int $count) は、対象のメソッドが最大で$count回実行される場合に一致するマッチャーを返します。
- exactly(int $count) は、対象のメソッドが正確に$count回実行される場合に一致するマッチャーを返します。
モックビルダー API
前述のように、createStub() と createMock() メソッドで使用されるデフォルトが要件に合わない場合、getMockBuilder($type) メソッドを使用してテストダブルの生成をカスタマイズすることができます。この際、フルエントインターフェースを使用します。Mock Builder で提供されるメソッドについては以下に記載します。
setMockClassName()
setMockClassName($name):生成されるテストダブルクラスに対してクラス名を指定するために使用します。
setConstructorArgs()
setConstructorArgs(array $args):元のクラスのコンストラクタに渡すパラメータ配列を指定するために使用します(デフォルトではダミー実装に置き換えられない)。
disableOriginalConstructor()
disableOriginalConstructor():元のクラスのコンストラクタ呼び出しを無効にするために使用します。
enableOriginalConstructor()
enableOriginalConstructor():元のクラスのコンストラクタを呼び出すように明示するために使用します(デフォルトの動作)。
disableOriginalClone()
disableOriginalClone():元のクラスのクローンコンストラクタ呼び出しを無効にするために使用します。
enableOriginalClone()
enableOriginalClone():元のクラスのクローンコンストラクタを呼び出すように明示するために使用します(デフォルトの動作)。
enableArgumentCloning()
enableArgumentCloning():ダブルドメソッドに渡される引数のクローニングを有効にするために使用します。
disableArgumentCloning()
disableArgumentCloning():ダブルドメソッドに渡される引数のクローニングを無効にするために使用します(デフォルトの動作)。
disableAutoReturnValueGeneration()
disableAutoReturnValueGeneration():設定されていない場合に自動的に返り値を生成する機能を無効にするために使用します。
enableAutoReturnValueGeneration()
enableAutoReturnValueGeneration():設定されていない場合に自動的に返り値を生成する機能が有効であることを明示するために使用します(デフォルトの動作)。
disallowMockingUnknownTypes()
disallowMockingUnknownTypes():不明な型のダブルを禁止するために使用します。
allowMockingUnknownTypes()
allowMockingUnknownTypes():不明な型のダブルが許可されることを明示するために使用します(デフォルトの動作)。
disableAutoload()
disableAutoload():テストダブルクラスの生成時に PHP のオートロード機能を無効にすることができます。
enableAutoload()
enableAutoload():PHP のオートロード機能を有効にします (これはデフォルトの動作です)。
enableProxyingToOriginalMethods()
enableProxyingToOriginalMethods():オリジナル・メソッドの呼び出しを有効にできます。オリジナル・メソッドの呼び出しに使用するオブジェクトは、 setProxyTarget() を使用して設定する必要があります。
disableProxyingToOriginalMethods()
disableProxyingToOriginalMethods():元のメソッドを呼び出さないことを明示できます (これがデフォルトの動作です)。
onlyMethods()
onlyMethods(array $methods):Mock Builderオブジェクトでこのメソッドを呼び出すことで、設定可能なテストダブルで置き換えるメソッドを指定できます。他のメソッドの振る舞いは変更されません。指定されたメソッドはモック化されるクラスに存在する必要があります。
addMethods()
addMethods(array $methods):Mock Builderオブジェクトでこのメソッドを呼び出すことで、モック化されるインターフェースまたはクラスに存在しないメソッドを指定できます。インターフェースまたはクラスに存在するメソッドは変更されません。
getMock()
getMock() メソッドは、前のメソッド呼び出しで行った設定に基づいてモックオブジェクトを生成し、返します。getMock() メソッドの呼び出しは、メソッドチェーンの最後でなければなりません。
getMockForAbstractClass()
getMockForAbstractClass(): getMockForAbstractClass() メソッドは、前のメソッド呼び出しで行った設定に基づいてモックオブジェクトを生成し、返します。getMockForAbstractClass() メソッドの呼び出しは、メソッドチェーンの最後でなければなりません。
getMockForTrait()
getMockForTrait() メソッドは、前のメソッド呼び出しで行った設定に基づいてモックオブジェクトを生成し、返します。getMockForTrait() メソッドの呼び出しは、メソッドチェーンの最後でなければなりません。
ここでは、モックビルダー の流暢なインターフェイスを使用してテストスタブの作成方法を設定する例を示します。このテスト・ダブルの設定には、 createStub() や createMock() で使用されているベスト・プラクティスのデフォルトを使用しています。
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
final class MockBuilderExampleTest extends TestCase
{
public function testStub(): void
{
// Create a stub for the SomeClass class.
$stub = $this->getMockBuilder(SomeClass::class)
->disableOriginalConstructor()
->disableOriginalClone()
->disableArgumentCloning()
->disallowMockingUnknownTypes()
->getMock();
// Configure the stub.
$stub->method('doSomething')
->willReturn('foo');
// Calling $stub->doSomething() will now return
// 'foo'.
$this->assertSame('foo', $stub->doSomething());
}
}
スタブとモックの違い
スタブ(Stub)とモック(Mock)は、テストダブルとして使用されるテスト対象のオブジェクトを置き換えるためのテスト用のオブジェクトです。これらは、テストケースを実行する際に、テスト対象のオブジェクトとの間で非現実的な依存関係を断ち切ることに役立ちます。しかし、スタブとモックには異なる目的と振る舞いがあります。
- スタブ(Stub): スタブは、テスト対象のオブジェクトが返す値を予め指定しておくことができるテスト用オブジェクトです。テストケースを実行する際に、スタブはテスト対象のオブジェクトの特定のメソッドが呼び出された場合に、指定した値を返します。スタブは主に、メソッドの返り値をテストするために使用されます。具体的には、テスト対象のオブジェクトが外部依存関係を持ち、その外部依存関係の振る舞いをシミュレートする場合に使用されます。
- モック(Mock): モックは、テスト対象のオブジェクトが特定のメソッドが呼び出されることを検証するためのテスト用オブジェクトです。モックはテストケースを実行する際に、テスト対象のオブジェクトに対して特定のメソッドの呼び出しを期待します。そして、そのメソッドが実際に呼び出されたかどうかを検証します。モックは主に、テスト対象のオブジェクトの振る舞いが正しいことを確認するために使用されます。具体的には、テスト対象のオブジェクトが他のオブジェクトと適切にやり取りすることを確認する場合に使用されます。
スタブとモックは、テストケースでのテストの粒度やテストの目的に応じて適切に使い分けることが重要です。スタブとモックの適切な使い方により、効果的なテストケースを作成することができます。
おわりに
PHPUnitにおけるモックについてご紹介しました。
何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント