モックとは
あるモジュールのテストを行いたいが、モジュール内で使用する別モジュールがまだ実装できてない場合、代用品が必要となる。
その役割を担ってくれるのがモック。
使い方
テスト対象は以下のようなものを想定。
namespace namespace App\Services;
use App\Repositories\SampleRepository;
class SampleService
{
public function __construct(SampleRepository $repository)
{
$this->repository = $repository;
}
public function findById($id)
{
return $this->repository->findById($id);
}
public function hoge($id)
{
$hoge = $this->repository->hoge($id);
//色々な処理
return $hoge;
}
public function fuga($id)
{
$hoge = $this->repository->fuga($id);
//色々な処理
return $fuga;
}
}
namespace App\Repositories;
use App\Models\Sample;
class SampleRepository
{
public function __construct(Sample $model)
{
$this->model = $model;
}
//コンストラクタを利用したメソッド
public function findById($id)
{
return $this->model->find($id);
}
//実装済みメソッド
public function hoge($id)
{
//色々な処理
return $hoge;
}
//未実装のメソッド
public function fuga($id)
{
}
}
この状態でSampleServiceのテストを行いたいが、Repositoryのfugaメソッドがまだ出来てないため、モックを作成する必要がある。
通常のモック
use App\Services\SampleService;
use App\Repositories\SampleRepository;
use App\Models\Sample;
use Mockery;
use Tests\TestCase;
class SampleServiceTest extends TestCase
{
public function setUp():void
{
parent::setUp();
$this->model = new Sample();
$this->model->name = 'sample_name';
$this->mock = $this->mock(SampleRepository::class, function(MockInterface $mock) {
//shouldReceive()で実行するメソッドを定義
//andReturnで返す値を定義
$mock->shouldReceive('fuga')->andReturn('fuga');
//withArgsで受け取ったメソッドに応じて返す値を変更可能
$mock->shouldReceive('findById')->withArgs([1])->andReturn($this->model);
$mock->shouldReceive('findById')->withArgs([2])->andReturn(null);
//テスト対象で使用するメソッドは全て定義しておかないと、エラーになる
$mock->shouldReceive('hoge')->andReturn('hoge');
}
$this->service = app(SampleService::class);
}
public function tearDown():void
{
parent::tearDown();
Mockey::close();
}
/**
* @test
*/
public function findById()
{
$result = $this->service->findById(1);
//色々なアサートメソッド
}
/**
* @test
*/
public function hoge()
{
$result = $this->service->hoge(1);
//色々なアサートメソッド
}
/**
* @test
*/
public function fuga()
{
$result = $this->service->fuga(1);
//色々なアサートメソッド
}
}
でも全てのメソッド定義しなきゃいけないのめんどくさいな...、実装済みのは実体使いたい...って時はpartialMockがおすすめ。
定義したもの以外は、通常通りメソッドを実行してくれる。
partialなモック
- $this->mock = $this->mock(SampleRepository::class, function(MockInterface $mock) {
- $mock->shouldReceive('fuga')->andReturn('fuga');
- $mock->shouldReceive('findById')->withArgs([1])->andReturn($this->model);
- $mock->shouldReceive('findById')->withArgs([2])->andReturn(null);
- $mock->shouldReceive('hoge')->andReturn('hoge');
- }
+ $mock = $this->partialMock(SampleRepository::class, function (MockInterface $mock) {
+ $mock->shouldReceive('fuga')->andReturn('fuga');
+ });
ただこれにも少し注意点があって、コンストラクタが実行されない。
そのため、上記のコードではfindByIdのテストが失敗する。
それを防ぐために対象クラスをインスタンス化する際、ちゃんと引数を渡す必要がある。
$this->mock = Mockery::mock(RepositoryService::class, [app(SampleModel::class)])->makePartial();
$this->mock->shouldReceive('fuga')->andReturn('fuga');
$this->service = new SampleService($this->mock);
私見
正直作るの手間だし、メンテも手間だしで個人的にあまり好きじゃない。
単体テストはそのクラス内で完結すべきってことを考えると思想として正しいのは理解できるんだけど、どちらかというと必要悪という認識。
勿論実態使用すると凄い時間がかかるとか、特定の出力のために複雑な条件を設定しなきゃいけないとかそういう場合は別だけど...。
ただ単体テストだけやって結合テストやってないのに、モック使ってたりするプロジェクトもあったので、やはりどちらかというと否定寄り。
「単体テスト、結合テスト、総合テスト全てちゃんとやれてるよ」ってとこが羨ましいなー。