0. はじめに
今回はテスト時に重要な「モック化」の手段をいくつか紹介していきます。Laravelのドキュメントでは主に機能テストに関する解説が多い印象を受けますが、「モック」は機能/ユニットテストなどテスト規模に関係なく役立ちます。
動作検証環境は以下の通りです。
- Laravel: 5.5 LTS
- php: 7.1
本エントリでは前半でモックに関する基礎的な内容を扱い、後半でLaravelを使ったアプリケーションのテストコードを書く際に役立つモック手法を3通り紹介します。
1. モックに関する基礎知識
1.1 基礎的な解釈
「テストを書く」ということは以下のプロセスで記述していきます。
- テスト条件を設定
- テスト対象を実行
- 実行結果を検証
モックとは主にコード内のロジックを検証するために必要な「1. テスト条件を設定」するための手段です。
テスト対象が別クラスに依存している場合、テスト条件はそれらのインスタンスの振る舞いによって決まることが多いでしょう。モックは依存クラスの振る舞いをテスト時に、開発者が定義したものに「すり替える」ことを指します。
1.2 「依存」の具体例
オブジェクト指向で設計されている場合、一つのクラス内で全ての処理が完結する場合は少ないでしょう。各クラスに役割が分担され、それぞれに処理が委譲されているはずです。
また、一般的なアプリケーションであれば以下のような外部ツールとの連携もあることが多いでしょう。
- DBやキャッシュなどのデータストレージ
- メッセージングキューを使った非同期処理
- ファイルへのログ出力
- 外部APIコール
etc..
一般的なアプリケーションはこれらの"状態"によってビジネスロジックが生まれていきます。「依存」とはそれらの"状態"によってコードの振る舞いが変わることを指します。
1.3 ユニットテストと機能テストの"依存"
ここで各種テストにおける"依存"を大まかにまとめると、以下のように分類できます。
ユニットテスト
テスト対象: 1クラスの1メソッド単位
モック対象: メソッド内部でコールしている他クラスのメソッド
機能テスト
テスト対象: URLへリクエスト→レスポンス(View or Response)を通した一連の処理
モック対象: プロセス内で発生する外部連携部分
モック対象がユニットテストと機能テストで異なるように見えますが、「外部連携」部分には何かしらのクラスのメソッドをコールしていると思いますのでユニットテストも機能テストも本質的には同じです。モック対象クラスのカテゴリとして、外部連携部分のクラスが多いということです。
--- 重要 ---
個人的に、テストコードはシステム外の状態(実行環境・サーバー内のファイルシステム状態・DB etc.)に依存せず、常に同じ結果となるべきだと考えています。Laravelであればプロジェクトディレクトリ内のソースコード+PHPのみで実行結果が確定することが理想です。
特に機能テストを重視してテストケースが増えていく過程でありがちなことは、外部連携部分をモックせずにテスト環境用にDBやモックAPIサーバーを準備しテストを運用していくことです。
よりE2Eテストに近いことをテストコードで実現するという意味では、堅牢であるといえるかもしれません。しかし同時に、それらの連携部分がテスト実行時に失敗の原因となりうる可能性もあります。
上記の手法をとるのであれば外部連携部分はユニットテストで動作保証をし、機能テストではその部分をモックしていくのがオススメです。理想的にはユニットテストと機能テストを完全に網羅することですが、開発コストとのバランスで「ユニットテスト+重要なシナリオを含んだ機能テスト」程度が現実的なところではないでしょうか。
いずれにせよ、チーム内でのテストコードの設計方針について議論が必要です。
---------------
1.4 モック・スタブ・スパイの違い
ひと口に「モック」するといっても実際にはもう少し詳細な役割分担があります。それぞれ、スタブ・モック・スパイです。
テストコードを書いた経験のある方であれば、一度は聞いたことのある単語でしょう。
これらの違いに関する詳細な説明はStack Overflowにスレッドが立てられていたりもしますので興味のある方は一度目を通してください。
こちらでは私自身の解釈で簡単にまとめていきます。
- スタブ: メソッドの返り値を指定
- モック: メソッドの返り値の指定 + 呼び出し引数・回数の検証
- スパイ: メソッドの呼び出し引数・回数の検証
いずれも実際に実行されるべきクラスをモッククラスに入れ替えることで実現可能な機能です。
モック機能はテストフレームワークから提供されていたり、様々なライブラリの開発が進んでいますので詳細はそれらのドキュメントを読むと各機能の理解が進みます。
「1.1 基礎的な解釈」において **"主に"**という表現をしたのは「3. 実行結果を検証」もモックの役割の一部であるためです。
2.代表的なモック手法 3選
2.1 Facadeを利用
まずは最もシンプルな手法として、Laravelから提供されているIlluminate\Support\Facade
クラスを継承した自前のクラスを定義することでモックの実現が可能です。こちらのソースコードからわかるように、Mockライブラリで提供されているようなメソッドがすでに定義されています。
内部的にはMockeryというライブラリで提供されたモッククラスを使用しています。
// こちらのFacadeの実体クラスにsend(string $arg): void
// というメソッドが定義されていると仮定します。
// (使い方) OriginalFacade::send('Hello World');
class OriginalFacade extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'original';
}
}
class TestTargetClass
{
public function sendWithPostfix(string $arg): bool
{
if ($arg === '') {
return false;
}
OriginalFacade::send("{$arg}!!!!!");
return true;
}
}
// 親クラスのTestCaseはLaravelで定義されたTests\TestCaseです。
class TargetClassTest extends TestCase
{
public function testSendWithPostfix_SuccessfullySendMessage(): void
{
// テスト条件を設定
// OriginalFacadeをモック
// 呼び出し引数・回数の期待値設定+返り値の指定
OriginalFacade::shouldReceive('send')
->once()
->with('Hello World!!!!!')
->andReturnNull();
// テスト対象の実行
$target = new TestTargetClass();
$result = $target->sendWithPostfix('Hello World');
// 実行結果を検証
$this->assertTrue($result);
}
public function testSendWithPostfix_FailedToSendMessage(): void
{
// テスト対象の実行
$target = new TestTargetClass();
$result = $target->sendWithPostfix('');
// 実行結果の検証
$this->assertFalse($result);
}
}
Facadeを使った場合、上記のように簡単にモックが可能です。しかし、Facadeクラスの弱点は部分的なモック(パーシャルモック)ができないことです。
2.2 サービス(DI)コンテナを利用
次に紹介するのは、先程Facadeクラスの弱点も補える万能型です。経験上、Laravelに関してはこの手法だけで全て事足りるように思います。
良い例が思い浮かばなかったのですが、以下のようにサービスコンテナ経由でのインスタンス化されたものをモックへすり替えることが可能です。
以前「Laravelサービスコンテナの仕組みとシングルトンの罠」という記事内で簡単にサービスコンテナの仕組みについて触れたので、合わせて読んでいただけると幸いです。
class TargetService
{
public function get(): string
{
$handler = app(DataHandler::class);
return $handler->getFromCache() ?: $handler->getFromDB();
}
}
class DataHandler
{
public getFromDB(): string
{
return 'dummy result from DB';
}
public getFromCache(): string
{
return 'dummy result from Cache';
}
}
class TargetServiceTest extends TestCase
{
public function testGet_readDbData(): void
{
// テスト条件を設定
// 部分的にモック
// 'getFromCache'以外は通常の定義通り動作
$mock = \Mockery::mock(DataHandler::class)->makePartial();
$mock->shouldReceive('getFromCache')
->once()
->withNoArgs()
->andReturn('');
// サービスコンテナ経由で元々登録されていた依存解決ルールをすり替える
$this->instance(DataHandler::class, $mock);
// テスト対象の実行
$target = new TargetService();
$result = $target->get();
// 実行結果を検証
$this-> assertEquals('dummy result from DB', $result);
}
}
2.3 引数からの依存注入
最後はDIコンテナを利用しないクラシックな手法です。クラシックではありますが依存注入の基本ですので、よりオブジェクト指向を意識したコードの書き方が自然に身につくでしょう。
こちらに関しては2通りやり方があります。option1が通常の「モック」で一般的な手法です。option2は「スタブ」としてReadable
を実装した同型のダミークラスを渡すことで、任意の実装を注入することが可能です。
interface Readable
{
public getFromDB(): string;
public getFromCache(): string;
}
class DataHandler implements Readable
{
public getFromDB(): string
{
return 'dummy result from DB';
}
public getFromCache(): string
{
return 'dummy result from Cache';
}
}
class TargetService
{
public function get(Readable $handler): string
{
return $handler->getFromCache() ?: $handler->getFromDB();
}
}
// option1
class TargetServiceTest1 extends TestCase
{
public function testGet_readDbData(): void
{
// テスト条件を設定
$mock = \Mockery::mock(DataHandler::class)->makePartial();
$mock->shouldReceive('getFromCache')
->once()
->withNoArgs()
->andReturn('');
// テスト対象の実行
$target = new TargetService();
$result = $target->get($mock);
// 実行結果を検証
$this-> assertEquals('dummy result from DB', $result);
}
}
// option2
class TargetServiceTest2 extends TestCase
{
public function testGet_readDbData(): void
{
// テスト対象の実行
$target = new TargetService();
$result = $target->get(new MockDataHandler());
// 実行結果を検証
$this-> assertEquals('dummy result from DB', $result);
}
}
class MockDataHandler implements Readable
{
public getFromDB(): string
{
return 'dummy result from DB';
}
public getFromCache(): string
{
return '';
}
}
まとめ
いかがでしたでしょうか。今回はLaravelアプリにおけるテストコードで使えるモック手法について簡単にまとめていきました。大変恐縮ですが、動作未検証のコードのため何か間違いがある可能性もありますのでご了承ください。