以前「Laravel:mockにおける引数と戻り値の関係」と言う記事で以下のような例を示したことがあります。
<?php
namespace App\Services\Sample;
class Test1Service
{
public function __invoke(array $serviceInput)
{
$Test1Repository = app()->make(\App\Repositories\Test1Repository::class);
$resultObj = new \stdClass;
$resultObj->result1 = $Test1Repository(1); // 戻り値は10を期待
$resultObj->result2 = $Test1Repository(2); // 戻り値は200を期待
return $resultObj;
}
}
上記に対して以下のようなmockを定義します。
/**
* @test
*/
public function Sample_Test1Controller_基本() : void
{
$this->mock(\App\Repositories\Test1Repository::class, function($mock) {
$mock->shouldReceive('__invoke')->with(1)->andReturn(10);
$mock->shouldReceive('__invoke')->with(2)->andReturn(200);
});
// 実行
$response = $this->get(route('sample.test1'));
// 確認
$response->assertStatus(200);
$expected = [
'data' => [
'result1' => 10,
'result2' => 200,
],
];
$response->assertExactJson($expected);
}
同一クラス・同一メソッドの呼び出しに対して、引数ごとに戻り値を変えることができると言うものです。
上記はandReturnUsingと言うメソッドを使って以下のように書くこともできます。
/**
* @test
*/
public function Sample_Test1Controller_基本() : void
{
$this->mock(\App\Repositories\Test1Repository::class, function($mock) {
$mock->shouldReceive('__invoke')->andReturnUsing(function ($arg) {
switch ($arg) {
case 1: return 10;
case 2: return 200;
}
});
});
// 実行
$response = $this->get(route('sample.test1'));
// 確認
$response->assertStatus(200);
$expected = [
'data' => [
'result1' => 10,
'result2' => 200,
],
];
$response->assertExactJson($expected);
}
andReturnUsingは本来は「メソッドへ渡された引数に基づいて返り値を計算したい場合」に対応するために用意されたメソッドのようですが、「計算」を額面通りに受け取る必要もなく、要は引数に基づいて戻り値を操作できるメソッドだと考えて良いかと思います。
ただ上記程度の用法であれば先に示したandReturnを列記する方法の方が簡単に思えます。
実はandReturnUsingの本当の価値は、同メソッドの引数がクロージャである点にあると思われます。
状態変更を伴う処理の代替
mockで代替しようとしている処理内で状態の変更が期待される場合もあります。
以下のようなケースを考えてみましょう。
<?php
namespace App\Services\Sample;
class Test1Service
{
public function __invoke($serviceInput)
{
$TChargeModel = $serviceInput['TChargeModel'];
$TChargeModel->status = 1;
$TChargeModel->save();
$Test1Repository = app()->make(\App\Repositories\Test1Repository::class);
$Test1Repository($TChargeModel);
$TChargeModel->refresh();
if ($TChargeModel->status !== 2) {
throw new \Exception('例外発生', 999);
}
return true;
}
}
上記ではTChargeModelエンティティのstatusの初期値として1を設定しますが、その後Test1Repository内での処理が正常に実行できた場合は状態が2に変更されていることが期待され、そうでない場合は異常として例外処理を行っています。
このようなケースでTest1Repositoryをmockで代替しようと思った場合、単に戻り値を指定できるだけのandReturnでは対応しきれず、andReturnUsingを使いたくなります。
/**
* @test
*/
public function Sample_Test1Controller_基本() : void
{
$this->mock(\App\Repositories\Test1Repository::class, function($mock) {
$mock->shouldReceive('__invoke')->andReturnUsing(function ($TChargeModel) {
$TChargeModel->status = 2;
$TChargeModel->save();
});
});
// 実行
$response = $this->get(route('sample.test1'));
// 確認
$response->assertStatus(200);
}
andReturnUsingなるメソッドでありながら戻り値がなかったり、そもそも状態変更をリポジトリ側に持ち込むのが不適切では?というツッコミ所があったりと色々不適切な点はあるかと思いますが、あくまで副作用を伴うメソッドの呼び出しをmockで代替しようとした場合の例としてご理解ください。
蛇足ながら、例外発生のテストをしたければ、andReturnUsingのクロージャ内で指定している状態を2以外にすれば良い訳です。
他のアクションの呼び出し
前述のケースの発展系として別のアクション呼び出しをシミュレートすることもできます。
例えば決済系の処理などで見かける、以下のようなパターンを考えてみます。
- 決済アクションが呼び出される
- 決済アクション内では外部機能(決済代行会社のAPI等)に決済要求を実行する
- 外部機能側の処理が正常に行われると自サイト側のコールバック処理が呼び出される
- コールバック処理では決済の正常終了に合わせて状態変更し、所定の応答を外部機能側に返す
- 外部機能側はコールバックからの戻りを受けて、所定の応答を呼び出し元の決済アクションに返す
- 決済アクション側は外部機能からの戻りを受けて、決済が正常に終了しているかどうかを状態確認し、その結果に従って処理を継続する
この一連の流れのポイントは以下の二点です。
- 決済アクションの処理中に外部機能呼び出しから間接的に自サイト側のコールバック(別アクション)が実行される。
つまり決済アクションの処理開始後、終了するまでの間に別のアクションが実行され、かつその完了を待ち合わせると言うアクション実行の入れ子状態が発生する。 - 外部機能呼び出し完了後の決済アクションの処理内容は間接的に実行されたコールバック(別アクション)の処理内容に依存する。
先に示したTest1Repository内で上記外部機能の呼び出しが行われ、その結果間接的にコールバック(settlement/complete)が呼び出されるものとし、そのTest1Repositoryをmockで代替しつつ、コールバックに関しては本来の処理を呼び出してみようと思った場合、以下のように書くことができます。
/**
* @test
*/
public function Sample_Test1Controller_基本() : void
{
$this->mock(\App\Repositories\Test1Repository::class, function($mock) {
$mock->shouldReceive('__invoke')->andReturnUsing(function ($TChargeModel) {
$this->get(route('settlement.complete'));
});
});
// 実行
$response = $this->get(route('sample.test1'));
// 確認
$response->assertStatus(200);
}
上記のように一般的にテストケース内で使用しているアクション呼び出しがandReturnUsingのクロージャ内からも実行可能です。
先の例にあったようにTest1Repository呼び出し完了後にTChargeModelエンティティのstatusが2に変更されていることが期待されるものとして、その変更が別アクション(settlement/complete)内で実施されるものであった場合も上記のような方法でテストすることができます。
テストケースの書き方としては上記のように別アクションを呼び出すのではなく、同アクション実行結果として発生する状態変化をクロージャ内で代替する方が正当なのかもしれませんが、自動テストの一環として関連する一連の処理を通しで(結合的に)確認するテストケースがあっても良いように思いますので、このようなやり方もアリかと思います。
所感
途中でも書きましたが、andReturnUsingはその名前が示す通り本来は「メソッドへ渡された引数に基づいて返り値を計算したい場合」に用いることを想定したものかと思いますが、クロージャが指定できることから今回紹介したような拡張的な使い方も可能になります。
それが正当な使い方かどうかは分かりませんが、あくまでテストケースの書き方に関してなので、とりあえずやりたいことがやれればOKということで。